做者:邊順html
單元測試(unit testing),是指對軟件中的最小可測試單元進行檢查和驗證前端
須要訪問數據庫的測試不是單元測試
須要訪問網絡的測試不是單元測試
須要訪問文件系統的測試不是單元測試
--- 修改代碼的藝術
複製代碼
在實踐工做中,進行了完整計劃的單元測試和編寫實際的代碼所花費的精力大體上是相同的node
兩個經常使用的單元測試方法論:react
一般是針對函數、模塊、對象進行測試jquery
至少須要三類工具來進行單元測試:ios
Jasmine:Behavior-Drive development(BDD)風格的測試框架,在業內較爲流行,功能很全面,自帶 asssert、mock 功能git
Qunit:該框架誕生之初是爲了 jquery 的單元測試,後來獨立出來再也不依賴於 jquery 自己,可是其身上仍是脫離不開 jquery 的影子github
Mocha:node 社區大神 tj 的做品,能夠在 node 和 browser 端使用,具備很強的靈活性,能夠選擇本身喜歡的斷言庫,選擇測試結果的 report數據庫
Jest:來自於 facebook 出品的通用測試框架,Jest 是一個使人愉快的 JavaScript 測試框架,專一於簡潔明快。他適用但不侷限於使用如下技術的項目:Babel, TypeScript, Node, React, Angular, Vuenpm
一般測試文件名與要測試的文件名相同,後綴爲.test.js
,全部測試文件默認放在__test__
文件夾中
describe
塊之中,提供測試用例的四個函數:before()、after()、beforeEach()和 afterEach()。它們會在指定時間執行(若是不須要能夠不寫)
測試文件中應包括一個或多個describe
, 每一個 describe 中能夠有一個或多個it
,每一個describe
中能夠有一個或多個expect
.
describe 稱爲"測試套件"(test suite),it 塊稱爲"測試用例"(test case)。
expect
就是判斷源碼的實際執行結果與預期結果是否一致,若是不一致就拋出一個錯誤.
全部的測試都應該是肯定的。 任什麼時候候測試未改變的組件都應該產生相同的結果。 你須要確保你的快照測試與平臺和其餘不相干數據無關。
基礎模板
describe('加法函數測試', () => {
before(() => {
// 在本區塊的全部測試用例以前執行
});
after(() => {
// 在本區塊的全部測試用例以後執行
});
beforeEach(() => {
// 在本區塊的每一個測試用例以前執行
});
afterEach(() => {
// 在本區塊的每一個測試用例以後執行
});
it('1加1應該等於2', () => {
expect(add(1, 1)).toBe(2);
});
it('2加2應該等於4', () => {
expect(add(2, 2)).toBe(42);
});
});
複製代碼
經常使用的測試
組件中的方法測試
it('changeCardType', () => {
let component = shallow(<Card />);
expect(component.instance().cardType).toBe('initCard');
component.instance().changeCardType('testCard');
expect(component.instance().cardType).toBe('testCard');
});
複製代碼
模擬事件測試
經過 Enzyme 能夠在這個返回的 dom 對象上調用相似 jquery 的 api 進行一些查找操做,還能夠調用 setProps 和 setState 來設置 props 和 state,也能夠用 simulate 來模擬事件,觸發事件後,去判斷 props 上特定函數是否被調用,傳參是否正確;組件狀態是否發生預料之中的修改;某個 dom 節點是否存在是否符合指望
it('can save value and cancel', () => {
const value = 'edit';
const { wrapper, props } = setup({
editable: true,
});
wrapper.find('input').simulate('change', { target: { value } });
wrapper.setProps({ status: 'save' });
expect(props.onChange).toBeCalledWith(value);
});
複製代碼
使用 snapshot 進行 UI 測試
it('App -- snapshot', () => {
const renderedValue = renderer.create(<App />).toJSON();
expect(renderedValue).toMatchSnapshot();
});
複製代碼
寫一個單元測試你須要這樣作
一般測試文件名與要測試的文件名相同,後綴爲.test.js,全部測試文件默認放在test文件夾中,通常測試文件包含下列內容:
accordion 組件
// accordion.test.tsx
import { afterEach, beforeEach, describe, expect, jest, test } from '@jest/globals';
import Enzyme, { mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import toJSON from 'enzyme-to-json';
import JestMock from 'jest-mock';
import React from 'react';
import { Accordion } from '..';
Enzyme.configure({ adapter: new Adapter() }); // 須要根據項目的react版原本配置適配
describe('Accordion', () => {
// 測試套件,經過 describe 塊來將測試分組
let onChange: JestMock.Mock<any, any>; // Jest 提供的mock 函數,擦除函數的實際實現、捕獲對函數的調用
let wrapper: Enzyme.ReactWrapper;
beforeEach(() => {
// 在運行測試前作的一些準備工做
onChange = jest.fn();
wrapper = mount(
<Accordion onChange={onChange}> <Accordion.Item name='one' header='one'> two </Accordion.Item> <Accordion.Item name='two' header='two' disabled={true}> two </Accordion.Item> <Accordion.Item name='three' header='three' showIcon={false}> three </Accordion.Item> <Accordion.Item name='four' header='four' active={true} icons={['custom']}> four </Accordion.Item> </Accordion>
);
});
afterEach(() => {
// 在運行測試後進行的一些整理工做
wrapper.unmount();
});
// UI快照測試,確保你的UI不會因意外改變
test('Test snapshot', () => {
// 測試用例,須要提供詳細的測試用例描述
expect(toJSON(wrapper)).toMatchSnapshot();
});
// 事件測試
test('should trigger onChange', () => {
wrapper.find('.qtc-accordion-item-header').first().simulate('click');
expect(onChange.mock.calls.length).toBe(1);
expect(onChange.mock.calls[0][0]).toBe('one');
});
// 關鍵邏輯測試
//點擊頭部觸發展開收起
test('should expand and collapse', () => {
wrapper.find('.qtc-accordion-item-header').at(2).simulate('click');
expect(wrapper.find('.qtc-accordion-item').at(2).hasClass('active')).toBeTruthy();
});
// 配置disabled時不可展開
test('should not trigger onChange when disabled', () => {
wrapper.find('.qtc-accordion-item-header').at(1).simulate('click');
expect(onChange.mock.calls.length).toBe(0);
});
// 對全部的屬性配置進行測試
// 是否展現頭部左側圖標
test('hide icon', () => {
expect(wrapper.find('.qtc-accordion-item-header').at(2).children().length).toBe(2);
});
// 自定義圖標
test('custom icon', () => {
const customIcon = wrapper.find('.qtc-accordion-item-header').at(3).children().first();
expect(customIcon.getDOMNode().innerHTML).toBe('custom');
});
// 是否可展開多項
test('single expand', () => {
onChange = jest.fn();
wrapper = mount(
<Accordion multiple={false} onChange={onChange}> <Accordion.Item name='1'>1</Accordion.Item> <Accordion.Item name='2'>2</Accordion.Item> </Accordion>
);
wrapper.find('.qtc-accordion-item-header').at(0).simulate('click');
wrapper.find('.qtc-accordion-item-header').at(1).simulate('click');
expect(wrapper.find(Accordion).state().activeNames).toEqual(new Set(['2']));
});
test('mutiple expand', () => {
onChange = jest.fn();
wrapper = mount(
<Accordion multiple={true} onChange={onChange}> <Accordion.Item name='1'>1</Accordion.Item> <Accordion.Item name='2'>2</Accordion.Item> </Accordion>
);
wrapper.find('.qtc-accordion-item-header').at(0).simulate('click');
wrapper.find('.qtc-accordion-item-header').at(1).simulate('click');
expect(wrapper.find(Accordion).state().activeNames).toEqual(new Set(['1', '2']));
});
});
複製代碼
對一些異步和延時的處理
使用單個參數調用 done,而不是將測試放在一個空參數的函數,Jest 會等 done 回調函數執行結束後,結束測試
test('the data is peanut butter', done => {
function callback(data) {
try {
expect(data).toBe('peanut butter');
done();
} catch (error) {
done(error);
}
}
fetchData(callback);
});
複製代碼
模擬 setTimeout
// 提取utils方法,封裝一個sleep
export const sleep = async (timeout = 0) => {
await act(async () => {
await new Promise((resolve) => globalTimeout(resolve, timeout));
});
};
// 測試用例中調用
it('測試用例', async () => {
doSomething();
await sleep(1000);
doSomething();
});
複製代碼
mock 組件內系統函數的返回結果
對於組件內調用了 document 上的方法,能夠經過 mock 指定方法的返回值,來保證一致性
const getBoundingClientRectMock = jest.spyOn(
HTMLHeadingElement.prototype,
'getBoundingClientRect',
);
beforeAll(() => {
getBoundingClientRectMock.mockReturnValue({
width: 100,
height: 100,
top: 1000,
} as DOMRect);
});
afterAll(() => {
getBoundingClientRectMock.mockRestore();
});
複製代碼
直接調用組件方法
經過 wrapper.instance()獲取組件實例,再調用組件內方法,如:wrapper.instance().handleScroll() 測試系統方法的調用
const scrollToSpy = jest.spyOn(window, 'scrollTo');
const calls = scrollToSpy.mock.calls.length;
expect(scrollToSpy.mock.calls.length).toBeGreaterThan(calls);
複製代碼
使用屬性匹配器代替時間
當快照有時間時,經過屬性匹配器能夠在快照寫入或者測試前只檢查這些匹配器是否經過,而不是具體的值
it('will check the matchers and pass', () => {
const user = {
createdAt: new Date(),
id: Math.floor(Math.random() * 20),
name: 'LeBron James',
};
expect(user).toMatchSnapshot({
createdAt: expect.any(Date),
id: expect.any(Number),
});
});
複製代碼
expect:返回一個'指望‘的對象
toBe:使用 object.is 去判斷相等
toEqual:遞歸檢測對象或數組的每一個字段
not:測試相反的匹配
toBeNull:只匹配 null
toBeUndefined:只匹配 undefined
toBeDefined:與 toBeUndefined 相反
toBeTruthy:匹配任何 if 語句爲真
toBeFalsy:匹配任務 if 語句爲假
toBeGreaterThan:大於
toBeGreaterThanOrEqual:大於等於
toBeLessThan:小於
toBeLessThanOrEqual:小於等於
toBeCloseTo:比較浮點數相等
toMatch:匹配字符串
toContain:檢測一個數組或可迭代對象是否包含某個特定項
toThrow:測試某函數在調用時是否拋出了錯誤
// The mock function was called at least once
expect(mockFunc).toHaveBeenCalled();
// The mock function was called at least once with the specified args
expect(mockFunc).toHaveBeenCalledWith(arg1, arg2);
// The last call to the mock function was called with the specified args
expect(mockFunc).toHaveBeenLastCalledWith(arg1, arg2);
// All calls and the name of the mock is written as a snapshot
expect(mockFunc).toMatchSnapshot();
複製代碼
默認狀況下,一旦到達運行上下文底部 Jest 測試當即結束,使用單個參數調用 done,而不是將測試放在一個空參數的函數,Jest 會等 done 回調函數執行結束後,結束測試。
test('the data is peanut butter', (done) => {
function callback(data) {
try {
expect(data).toBe('peanut butter');
done();
} catch (error) {
done(error);
}
}
fetchData(callback);
});
複製代碼
爲你的測試返回一個 Promise,Jest 會等待 Promise 的 resove 狀態,若是 Promist 被拒絕,則測試將自動失敗
test('the data is peanut butter', () => {
return fetchData().then((data) => {
expect(data).toBe('peanut butter');
});
});
複製代碼
若是指望 Promise 被 Reject,則須要使用 .catch 方法。 請確保添加 expect.assertions 來驗證必定數量的斷言被調用。 不然,一個 fulfilled 狀態的 Promise 不會讓測試用例失敗
test('the fetch fails with an error', () => {
expect.assertions(1);
return fetchData().catch(e => expect(e).toMatch('error'));
});
複製代碼
test('the data is peanut butter', () => {
return expect(fetchData()).resolves.toBe('peanut butter');
});
test('the fetch fails with an error', () => {
return expect(fetchData()).rejects.toMatch('error');
});
複製代碼
寫異步測試用例時,能夠再傳遞給 test 的函數前面加上 async。
爲屢次測試重複設置:beforeEach、afterEach 來爲屢次測試重複設置的工做
一次性設置:beforeAll、afterAll 在文件的開頭作一次設置
做用域:能夠經過 describe 塊將測試分組,before 和 after 的塊在 describe 塊內部時,則只適用於該 describe 塊內的測試
Mock 函數容許你測試代碼之間的鏈接——實現方式包括:擦除函數的實際實現、捕獲對函數的調用(以及在這些調用中傳遞的參數)、在使用 new 實例化時捕獲構造函數的實例、容許測試時配置返回值。
兩種方法能夠模擬函數:1.在測試代碼中建立一個 mock 函數,2.編寫一個手動 mock 來覆蓋模塊依賴
const mockCallback = jest.fn((x) => 42 + x);
forEach([0, 1], mockCallback);
// 此 mock 函數被調用了兩次
expect(mockCallback.mock.calls.length).toBe(2);
// 第一次調用函數時的第一個參數是 0
expect(mockCallback.mock.calls[0][0]).toBe(0);
// 第二次調用函數時的第一個參數是 1
expect(mockCallback.mock.calls[1][0]).toBe(1);
// 第一次函數調用的返回值是 42
expect(mockCallback.mock.results[0].value).toBe(42);
複製代碼
全部的 mokc 函數都有這個特殊的.mock 屬性,它保存了關於此函數如何被調用、調用時的返回值的信息。.mock 屬性還追蹤每次調用時的 this 的值,因此咱們一樣能夠檢查 this
// 這個函數被實例化兩次
expect(someMockFunction.mock.instances.length).toBe(2);
// 這個函數被第一次實例化返回的對象中,有一個 name 屬性,且被設置爲了 'test’
expect(someMockFunction.mock.instances[0].name).toEqual('test');
複製代碼
const myMock = jest.fn();
console.log(myMock());
// > undefined
myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);
console.log(myMock(), myMock(), myMock(), myMock());
// > 10, 'x', true, true
複製代碼
能夠 用 jest.mock(...)函數自動模擬 axios 模塊,一旦模擬模塊,咱們可爲.get 提供一個 mockResolveValue,它會返回假數據用於測試
// users.test.js
import axios from 'axios';
import Users from './users';
jest.mock('axios');
test('should fetch users', () => {
const users = [{ name: 'Bob' }];
const resp = { data: users };
axios.get.mockResolvedValue(resp);
// or you could use the following depending on your use case:
// axios.get.mockImplementation(() => Promise.resolve(resp))
return Users.all().then((data) => expect(data).toEqual(users));
});
複製代碼
用 mock 函數替換指定返回值:jest.fn(cb => cb(null, true))
用 mockImplementation 根據別的模塊定義默認的 mock 函數實現:jest.mock('../foo'); const foo = require('../foo');foo.mockImplementation(() => 42);
當你須要模擬某個函數調用返回不一樣結果時,請使用 mockImplementationOnce 方法
.mockReturnThis()函數來支持鏈式調用
能夠爲你的 Mock 函數命名,該名字會替代 jest.fn() 在單元測試的錯誤輸出中出現。 用這個方法你就能夠在單元測試輸出日誌中快速找到你定義的 Mock 函數
const myMockFn = jest
.fn()
.mockReturnValue('default')
.mockImplementation((scalar) => 42 + scalar)
.mockName('add42');
複製代碼
當要確保你的 UI 不會又意外的改變時,快照測試是很是有用的工具;典型的作法是在渲染了 UI 組件以後,保存一個快照文件, 檢測他是否與保存在單元測試旁的快照文件相匹配。 若兩個快照不匹配,測試將失敗:有可能作了意外的更改,或者 UI 組件已經更新到了新版本。
快照文件應該和項目代碼一塊兒提交併作代碼評審
jest --updateSnapshot/jest -u
,這將爲全部失敗的快照測試從新生成快照文件。 若是咱們無心間產生了 Bug 致使快照測試失敗,應該先修復這些 Bug,再生成快照文件;只從新生成一部分的快照文件,你可使用--testNamePattern
來正則匹配想要生成的快照名字
項目中經常會有不定值字段生成(例如 IDs 和 Dates),針對這些狀況,Jest 容許爲任何屬性提供匹配器(非對稱匹配器)。 在快照寫入或者測試前只檢查這些匹配器是否經過,而不是具體的值
it('will check the matchers and pass', () => {
const user = {
createdAt: new Date(),
id: Math.floor(Math.random() * 20),
name: 'LeBron James',
};
expect(user).toMatchSnapshot({
createdAt: expect.any(Date),
id: expect.any(Number),
});
});
複製代碼
Jest 還提供了生成測試覆蓋率報告的命令,只須要添加上 --coverage 這個參數便可生成,再加上--colors 可根據覆蓋率生成不一樣顏色的報告(<50%紅色,50%~80%黃色, ≥80%綠色)
nzyme 來自 airbnb 公司,是一個用於 React 的 JavaScript 測試工具,方便你判斷、操縱和歷遍 React Components 輸出。Enzyme 的 API 經過模仿 jQuery 的 API ,使得 DOM 操做和歷遍很靈活、直觀。Enzyme 兼容全部的主要測試運行器和判斷庫。
shallow 淺渲染
返回組件的淺渲染,對官方 shallow rendering 進行封裝。淺渲染 做用就是:它僅僅會渲染至虛擬 dom,不會返回真實的 dom 節點,這個對測試性能有極大的提高。shallow 只渲染當前組件,只能能對當前組件作斷言
render 靜態渲染
將 React 組件渲染成靜態的 HTML 字符串,而後使用 Cheerio 這個庫解析這段字符串,並返回一個 Cheerio 的實例對象,能夠用來分析組件的 html 結構,對於 snapshot 使用 render 比較合適
mount 徹底渲染
將組件渲染加載成一個真實的 DOM 節點,用來測試 DOM API 的交互和組件的生命週期,用到了 jsdom 來模擬瀏覽器環境
.simulate(event, mock):用來模擬事件觸發,event 爲事件名稱,mock 爲一個 event object
.instance():返回測試組件的實例
.find(selector):根據選擇器查找節點,selector 能夠是 CSS 中的選擇器,也能夠是組件的構造函數,以及組件的 display name 等
.get(index):返回指定位置的子組件的 DOM 節點
.at(index):返回指定位置的子組件
.first():返回第一個子組件
.last():返回最後一個子組件
.type():返回當前組件的類型
.contains(nodeOrNodes):當前對象是否包含參數重點 node,參數類型爲 react 對象或對象數組
.text():返回當前組件的文本內容
.html():返回當前組件的 HTML 代碼形式
.props():返回根組件的全部屬性
.prop(key):返回根組件的指定屬性
.state([key]):返回根組件的狀態
.setState(nextState):設置根組件的狀態
.setProps(nextProps):設置根組件的屬性