使用 jest 單元測試

測試是軟件開發工做的重要一環,甚至有一種測試驅動開發(Test-Driven Development)的研發模式,要求整個研發工做是從編寫測試用例開始。測試根據不一樣的維度有多種分類方式,按照測試階段主要有單元測試、集成測試和系統測試,而單元測試是保障程序基本正確性的重中之重。html

單元測試(Unit Tesing)是針對程序的最小部件,檢查代碼是否會按照預期工做的一種測試手段。在過程式編程中最小就是一個函數,在面向對象編程中最小部件就是對象方法。前端

下文介紹使用 jest 對 Node.js 程序進行單元測試node

爲何選擇 jest

單元測試的執行一般須要測試規範、斷言、mock、覆蓋率工具等支持,上述工具在繁榮的 Node.js 生態中有不少優秀實現,但組合起來使用會帶來兩個問題react

  1. 多種工具的選擇和學習有必定的成本ios

  2. 把多個工具組合成特定測試解決方案的配置複雜git

而 Jest 是用來建立、執行和構建測試用例的 JavaScript 測試庫,自身包含了 驅動、斷言庫、mock 、代碼覆蓋率等多種功能,配置使用至關簡單github

安裝與配置

$ npm i --save-dev jest
複製代碼

把 jest 安裝到項目的 devDepecencies 後,在 package.json 添加配置正則表達式

"scripts": {
  "test": "jest"
}
複製代碼

這樣就可使用命令 npm test 執行測試代碼了typescript

根目錄下的 jest.config.js 文件能夠自定義 jest 的詳細配置,雖然 jest 相關配置也能夠在 package.json 內,但爲了可讀性推薦在獨立文件配置npm

小試牛刀

1.建立項目目錄

.
├── src
│   └── sum.js
├── test
│   └── sum.test.js
├── .gitignore
├── jest.config.js
├── README.md
└── package.json
複製代碼

2. 建立 src/sum.js

function sum(a, b) {
  return a + b;
}
module.exports = sum;
複製代碼

3. 建立 test/sum.test.js

const sum = require('../src/sum');

test('1 + 2 = 3', () => {
  expect(sum(1, 2)).toBe(3);
});
複製代碼

在測試用例中使用 expect(x).toBe(y) 的方式表達 x 與 y 相同,相似 Node.js 提供的 assert(x, y) 斷言,相對而言 jest 提供的語法有更好的語義性和可讀性

執行測試命令

$ npm test
複製代碼

image.png

jest 會自動運行

sum.test.js

文件,其默認匹配規則

  1. 匹配 __test__ 文件夾下的 .js 文件(.jsx .ts .tsx 也能夠)

  2. 匹配全部後綴爲 .test.js.spec.js 的文件(.jsx .ts .tsx 也能夠)

能夠經過根目錄下的 jest.config.js 文件自定義測試文件匹配規則

module.exports = {
  testMatch: [ // glob 格式
    "**/__tests__/**/*.[jt]s?(x)",
    "**/?(*.)+(spec|test).[jt]s?(x)"
  ],

  // 正則表達式格式,與 testMatch 互斥,不能同時聲明
  // testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.[jt]sx?$',
};
複製代碼

斷言

jest 提供了 BDD 風格的斷言支持,功能十分豐富,介紹幾個最經常使用的

相等

.toBe() 使用 Object.is 來測試兩個值精準相等

expect(2 + 2).toBe(4);
複製代碼

若是測試對象可使用 toEqual() ,遞歸檢查數組或對象的每一個字段

const data = {one: 1};
data['two'] = 2;
expect(data).toEqual({one: 1, two: 2});
複製代碼

添加 not 能夠表達相反匹配

expect(a + b).not.toBe(0);
複製代碼

真值

  • toBeNull 只匹配 null

  • toBeUndefined 只匹配 undefined

  • toBeDefinedtoBeUndefined 相反

  • toBeTruthy 匹配任何 if 語句爲真

  • toBeFalsy 匹配任何 if 語句爲假

    test('null', () => { const n = null; expect(n).toBeNull(); expect(n).toBeDefined(); expect(n).not.toBeUndefined(); expect(n).not.toBeTruthy(); expect(n).toBeFalsy(); });

    test('zero', () => { const z = 0; expect(z).not.toBeNull(); expect(z).toBeDefined(); expect(z).not.toBeUndefined(); expect(z).not.toBeTruthy(); expect(z).toBeFalsy(); });

數字

test('two plus two', () => {
  const value = 2 + 2;
  expect(value).toBeGreaterThan(3);
  expect(value).toBeGreaterThanOrEqual(3.5);
  expect(value).toBeLessThan(5);
  expect(value).toBeLessThanOrEqual(4.5);
});
複製代碼

對於比較浮點數相等,使用 toBeCloseTo 而不是 toEqual

test('兩個浮點數字相加', () => {
  const value = 0.1 + 0.2;
  expect(value).toBe(0.3); // 這句會報錯,由於浮點數有舍入偏差
  expect(value).toBeCloseTo(0.3); // 這句能夠運行
});
複製代碼

包含

能夠經過 toContain來檢查一個數組或可迭代對象是否包含某個特定項

expect(shoppingList).toContain('beer');
複製代碼

測試異步函數

jest 對幾種常見的異步方法提供了測試支持

src/async.js

module.exports = {
  cb: fn => {
    setTimeout(() => {
      fn('peanut butter');
    }, 300);
  },
  pm: () => {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve('peanut butter');
      }, 300);
    });
  },
  aa: async () => {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve('peanut butter');
      }, 300);
    });
  }
};
複製代碼

test/async.test.js

const { cb, pm, aa } = require('../src/async');
複製代碼

回調

test 方法的第二個函數傳入 done 能夠用來標識回調執行完成

test('callback data is peanut butter', done => {
  function callback(data) {
    try {
      expect(data).toBe('peanut butter');
      done();
    } catch (error) {
      done(error);
    }
  }

  cb(callback);
});
複製代碼

Promise

test('promise then data is peanut butter', () => {
  return pm().then(data => {
    expect(data).toBe('peanut butter');
  });
});
複製代碼

必定要把 Promise 作爲返回吃,不然測試用例會在異步方法執行完以前結束,若是但願單獨測試 resolve 可使用另一種書寫方式

test('promise resolve data is peanut butter', () => {
  return expect(pm()).resolves.toBe('peanut butter');
});
複製代碼

async/await

async/await 測試比較簡單,只要外層方法聲明爲 async 便可

test('async/await data is peanut butter', async () => {
  const data = await aa();
  expect(data).toBe('peanut butter');
});
複製代碼

任務鉤子

寫測試用例的時候常常須要在運行測試前作一些預執行,和在運行測試後進行一些清理工做,Jest 提供輔助函數來處理這個問題

屢次重複

若是在每一個測試任務開始前須要執行數據初始化工做、結束後執行數據清理工做,可使用 beforeEach 和 afterEach

beforeEach(() => {
  initializeCityDatabase();
});

afterEach(() => {
  clearCityDatabase();
});

test('city database has Vienna', () => {
  expect(isCity('Vienna')).toBeTruthy();
});

test('city database has San Juan', () => {
  expect(isCity('San Juan')).toBeTruthy();
});
複製代碼

一次性設置

若是相關任務全局只須要執行一次,可使用 beforeAll 和 afterAll

beforeAll(() => {
  return initializeCityDatabase();
});

afterAll(() => {
  return clearCityDatabase();
});

test('city database has Vienna', () => {
  expect(isCity('Vienna')).toBeTruthy();
});

test('city database has San Juan', () => {
  expect(isCity('San Juan')).toBeTruthy();
});
複製代碼

做用域

默認狀況下,before 和 after 的塊能夠應用到文件中的每一個測試。 此外能夠經過 describe 塊來將測試分組。 當 before 和 after 的塊在 describe 塊內部時,則其只適用於該 describe 塊內的測試

describe('Scoped / Nested block', () => {
  beforeAll(() => console.log('2 - beforeAll'));
  afterAll(() => console.log('2 - afterAll'));
  beforeEach(() => console.log('2 - beforeEach'));
  afterEach(() => console.log('2 - afterEach'));
  test('', () => console.log('2 - test'));
});
複製代碼

mock

在不少時候測試用例須要在相關環境下才能正常運行,jest 提供了豐富的環境模擬支持

mock 函數

使用 jest.fn() 就能夠 mock 一個函數,mock 函數有 .mock 屬性,標識函數被調用及返回值信息

const mockFn = jest.fn();
mockFn
    .mockReturnValueOnce(10)
  .mockReturnValueOnce('x')
  .mockReturnValue(true);

console.log(myMock(), myMock(), myMock(), myMock());
// > 10, 'x', true, true
複製代碼

mock 模塊

使用 jest.mock(模塊名) 能夠 mock 一個模塊,好比某些功能依賴了 axios 發異步請求,在實際測試的時候咱們但願直接返回既定結果,不用發請求,就能夠 mock axios

// src/user.js
const axios = require('axios');

class Users {
  static all() {
    return axios.get('/users.json').then(resp => resp.data);
  }
}

module.exports = Users;

// /src/user.test.js
const axios = require('axios');
const Users = require('../src/user');

jest.mock('axios'); // mock axios

test('should fetch users', () => {
  const users = [{ name: 'Bob' }];
  const resp = { data: users };
  
  // 修改其 axios.get 方法,直接返回結果,避免發請求
  axios.get.mockResolvedValue(resp);

  // 也能夠模擬其實現
  // axios.get.mockImplementation(() => Promise.resolve(resp));

  return Users.all().then(data => expect(data).toEqual(users));
});
複製代碼

babel & typeScript

如今不少前端代碼直接使用了 ES6 和 Typescript,jest 能夠經過簡單配置支持

安裝依賴

$ npm i -D babel-jest @babel/core @babel/preset-env @babel/preset-typescript @types/jest
複製代碼

添加 babel 配置

// babel.config.js
module.exports = {
  presets: [
    ['@babel/preset-env', { targets: { node: 'current' } }],
    '@babel/preset-typescript',
  ],
};
複製代碼

這樣測試用例也能夠用 ES6 + TypeScript 了

react 測試

安裝依賴

$ npm i -S react react-dom
$ npm i -D @babel/preset-react enzyme enzyme-adapter-react-16
複製代碼

配置 babel

// babel.config.js
module.exports = {
  presets: [
    ['@babel/preset-env', { targets: { node: 'current' } }],
    '@babel/preset-typescript',
    '@babel/preset-react',
  ],
};
複製代碼

編寫組件

// src/checkbox-with-label.js

import React, { useState } from 'react';

export default function CheckboxWithLabel(props) {
  const [checkStatus, setCheckStatus] = useState(false);

  const { labelOn, labelOff } = props;

  function onChange() {
    setCheckStatus(!checkStatus);
  }

  return (
    <label>
      <input
        type="checkbox"
        checked={checkStatus}
        onChange={onChange}
      />
      {checkStatus ? labelOn : labelOff}
    </label>
  );
}
複製代碼

編寫測試用例

react 測試有多種方式,在 demo 中使用最好理解的 enzyme

// test/checkbox-with-label.test.js

import React from 'react';
import Enzyme, { shallow } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

import CheckboxWithLabel from '../src/checkbox-with-label';

beforeAll(() => {
  // enzyme 初始化
  Enzyme.configure({ adapter: new Adapter() });
})

test('CheckboxWithLabel changes the text after click', () => {
  // 渲染組件
  const checkbox = shallow(<CheckboxWithLabel labelOn="On" labelOff="Off" />);
  expect(checkbox.text()).toEqual('Off');

  // 觸發事件
  checkbox.find('input').simulate('change');
  expect(checkbox.text()).toEqual('On');
});
複製代碼

測試覆蓋率

jest 還提供了測試覆蓋率的支持,執行命令 npm test -- --coverage 或者配置 package.json

"scripts": {
    "test": "jest",
    "coverage": "jest --coverage"
  }
複製代碼

執行命令 npm run coverage 便可

image.png

命令執行完成會在項目根目錄添加 coverage 文件夾,使用瀏覽器打開 coverage/lcov-report/index.html 文件,有可視化的測試報告

image.png

項目完整代碼:github.com/Samaritan89…

相關文章
相關標籤/搜索