測試是軟件開發工做的重要一環,甚至有一種測試驅動開發(Test-Driven Development)的研發模式,要求整個研發工做是從編寫測試用例開始。測試根據不一樣的維度有多種分類方式,按照測試階段主要有單元測試、集成測試和系統測試,而單元測試是保障程序基本正確性的重中之重。html
單元測試(Unit Tesing)是針對程序的最小部件,檢查代碼是否會按照預期工做的一種測試手段。在過程式編程中最小就是一個函數,在面向對象編程中最小部件就是對象方法。前端
下文介紹使用 jest 對 Node.js 程序進行單元測試node
單元測試的執行一般須要測試規範、斷言、mock、覆蓋率工具等支持,上述工具在繁榮的 Node.js 生態中有不少優秀實現,但組合起來使用會帶來兩個問題react
多種工具的選擇和學習有必定的成本ios
把多個工具組合成特定測試解決方案的配置複雜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
.
├── src
│ └── sum.js
├── test
│ └── sum.test.js
├── .gitignore
├── jest.config.js
├── README.md
└── package.json
複製代碼
function sum(a, b) {
return a + b;
}
module.exports = sum;
複製代碼
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
複製代碼
jest 會自動運行
sum.test.js
文件,其默認匹配規則
匹配 __test__
文件夾下的 .js 文件(.jsx .ts .tsx 也能夠)
匹配全部後綴爲 .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
toBeDefined
與 toBeUndefined
相反
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);
});
複製代碼
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 便可
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'));
});
複製代碼
在不少時候測試用例須要在相關環境下才能正常運行,jest 提供了豐富的環境模擬支持
使用 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
複製代碼
使用 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));
});
複製代碼
如今不少前端代碼直接使用了 ES6 和 Typescript,jest 能夠經過簡單配置支持
$ npm i -D babel-jest @babel/core @babel/preset-env @babel/preset-typescript @types/jest
複製代碼
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript',
],
};
複製代碼
這樣測試用例也能夠用 ES6 + TypeScript 了
$ npm i -S react react-dom
$ npm i -D @babel/preset-react enzyme enzyme-adapter-react-16
複製代碼
// 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
便可
命令執行完成會在項目根目錄添加 coverage
文件夾,使用瀏覽器打開 coverage/lcov-report/index.html
文件,有可視化的測試報告
項目完整代碼:github.com/Samaritan89…