1.背景
本文中的自動化測試指的是單元測試 (UT),所謂單元測試也就是對每個單元進行測試,通俗的將一般針對的是函數,類或單個組件,不涉及系統和集成。單元測試是軟件測試的基礎測試,主要是用來驗證所測代碼是否和程序員的期望一致。
jest 是 facebook 開源的,用來進行單元測試的框架,功能比較全面,測試、斷言、覆蓋率它都可以,另外還提供了快照功能。
2.安裝與配置
2.1安裝
安裝jest
npm install --save-dev jest
安裝babel-jest
npm install --save-dev babel-jest
安裝enzyme,需要根據項目的react版本來安裝對應的enzyme
npm install --save-dev enzyme enzyme-adapter-react-16
安裝react-test-renderer
npm install --save-dev react-test-renderer
2.2配置
package.json中添加:
{ "scripts": { "test": "jest" } }
執行npm run test 命令可在終端運行查看測試運行結果。
同時 Jest
還提供了生成測試覆蓋率報告的命令,只需要添加上 --coverage
這個參數既可生成,再加上--colors可根據覆蓋率生成不同顏色的報告(<50%紅色,50%~80%黃色, ≥80%綠色)
.babelrc文件中添加,請根據自己的項目情況調整
{ "env": { "test": { "presets": [["next/babel", { "preset-env": { "modules": "commonjs" }, "styled-jsx": { "plugins": [ "styled-jsx-plugin-postcss" ] } }]] } } }
jest.config.js: jest配置文件,可放在根目錄下或config文件下(也可以起其他名字或者直接寫在package.json里)
module.exports = { setupFiles: ['<rootDir>/jest.setup.js'], // 運行測試前可執行的腳本(比如注冊enzyme的兼容) transform: { '^.+\\.(js|jsx|mjs)$': '<rootDir>/node_modules/babel-jest', '^.+\\.css$': '<rootDir>/__test__/css-transform.js', }, testPathIgnorePatterns: ['<rootDir>/.next/', '<rootDir>/node_modules/'], //轉換時需忽略的文件 testURL: 'http://localhost/', // 運行環境下的URl };
還有一些配置, 詳細的配置見jest官網
collectCoverage: true, // 是否收集測試時的覆蓋率信息(默認是false,同package配置的--coverage參數) collectCoverageFrom: ['<rootDir>/src/**/*.{js,jsx,mjs}'], // 哪些文件需要收集覆蓋率信息 coverageDirectory: '<rootDir>/test/coverage', // 輸出覆蓋信息文件的目錄 coveragePathIgnorePatterns: ['/node_modules/', '<rootDir>/src/index.jsx'], // 統計覆蓋信息時需要忽略的文件 moduleNameMapper: { // 需要mock處理掉的文件,比如樣式文件 }, testMatch: [ // 匹配的測試文件 '<rootDir>/test/**/?(*.)(spec|test).{js,jsx,mjs}', '<rootDir>/src/**/__tests__/**/*.{js,jsx,mjs}', ],
jest.setup.js
/* eslint-disable import/no-extraneous-dependencies */ import { configure } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; configure({ adapter: new Adapter() });
3.測試
通常測試文件名與要測試的文件名相同,后綴為.test.js,所有測試文件默認放在__test__文件夾中。
describe塊之中,提供測試用例的四個函數:before()、after()、beforeEach()和afterEach()。它們會在指定時間執行(如果不需要可以不寫)
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);
});
});
測試文件中應包括一個或多個describe, 每個describe中可以有一個或多個it,每個describe中可以有一個或多個expect.
describe稱為"測試套件"(test suite),it塊稱為"測試用例"(test case)。
expect就是判斷源碼的實際執行結果與預期結果是否一致,如果不一致就拋出一個錯誤.
3.1簡單測試
import React from 'react'; export default () => ( <div>404</div> );
/* eslint-env jest */ import { shallow } from 'enzyme'; import React from 'react'; import Page404 from '../components/Page404'; describe('Page404', () => { it('Page404 shows "404"', () => { const app = shallow(<Page404 />); expect(app.find('div').text()).toEqual('404'); }); });
這個測試只測試了組件是否被正常顯示出來了。expect
部分是斷言,實現內容是在被渲染出的Page404組件中找到div標簽,然后斷言它的text()
中有沒有包含期望的文字。通過這種方式我們可以得知組件是否有被顯示出來。
除了text()
屬性以外,還可非常靈活的通過其他方式來得知組件是否被正常顯示。例如:
expect(wrapper.find('.card').exists()).toBeTruthy()
expect(wrapper.find('input').props().type).toBe('text')
npm test運行所有測試文件或 npm test <name> 運行匹配的測試文件:
-
% Stmts是語句覆蓋率(statement coverage):是否每個語句都執行了
-
% Branch分支覆蓋率(branch coverage):是否每個分支代碼塊都執行了(if, ||, ? : )
-
% Funcs函數覆蓋率(function coverage):是否每個函數都調用了
-
% Lines行覆蓋率(line coverage):是否每一行都執行了
在這里簡單介紹下enzyme
enzyme是Airbnb開源的react測試類庫,提供了一套簡潔強大的API,並通過jquery風格的方式進行dom處理,開發體驗十分友好. 它提供三種測試方法
shallow:
shallow 返回組件的淺渲染,對官方shallow rendering 進行封裝。淺渲染 作用就是:它僅僅會渲染至虛擬dom,不會返回真實的dom節點,這個對測試性能有極大的提升。shallow只渲染當前組件,只能能對當前組件做斷言
mount :
mount 方法用於將React組件加載為真實DOM節點。mount會渲染當前組件以及所有子組件
render:
render 采用的是第三方庫Cheerio
的渲染,渲染結果是普通的html結構,對於snapshot使用render
比較合適。
多數情況下,shallow
方法就能滿足我們的需求了。
Enzyme的一部分API,你可以從中了解它的大概用法。詳細的API
.get(index):返回指定位置的子組件的DOM節點
.at(index):返回指定位置的子組件
.first():返回第一個子組件
.last():返回最后一個子組件
.type():返回當前組件的類型
.text():返回當前組件的文本內容
.html():返回當前組件的HTML代碼形式
.props():返回根組件的所有屬性
.prop(key):返回根組件的指定屬性
.state([key]):返回根組件的狀態
.setState(nextState):設置根組件的狀態
.setProps(nextProps):設置根組件的屬性
例如:
expect(wrapper.find('input').prop('value')).toBe('default value');
3.2 模擬 Props,渲染組件創建 Wrapper
/* eslint-env jest */ import { shallow } from 'enzyme'; import React from 'react'; import { OrderManage } from '../../components/purchaser/OrderManege'; const setup = ({ ...props }) => { const wrapper = shallow(<OrderManage {...props} />); return { props, wrapper, }; };
describe('OrderManage', () => { it('role is operator', () => { const { wrapper } = setup({ role: 'operator', isFetching: true, fetchOrdersByStatuses: () => {}, // 直接設為空函數
getData: jest.fn(), // Jest 提供的mock 函數 }); const params = { node: { id: 2, }, }; expect(wrapper.instance().handlePageChange(1)); expect(wrapper.instance().OrderManagementLink(params)); expect(wrapper.find('.loader')).toHaveLength(1); expect(wrapper.find('.order-simpleGrid')).toHaveLength(0); expect(wrapper.type()).toEqual('div'); }); });
在正式測試功能之前,我們要寫一個 setup
方法用來渲染組件,因為每一個測試case都會用到它
3.3 組件中的方法測試
export class Card extends React.Component { constructor (props) { super(props) this.cardType = 'initCard' } changeCardType (cardType) { this.cardType = cardType } ... }
it('changeCardType', () => { let component = shallow(<Card />) expect(component.instance().cardType).toBe('initCard') component.instance().changeCardType('testCard') expect(component.instance().cardType).toBe('testCard') })
其中,instance
方法可以用於獲取組件的內部成員對象。
3.4 模擬事件測試
<Input value={value} onChange={e => this.handleChange(e)}/>
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); })
我們可以在這個返回的 dom 對象上調用類似 jquery 的api進行一些查找操作,還可以調用 setProps 和 setState 來設置 props 和 state,也可以用 simulate 來模擬事件,
觸發事件后,去判斷props上特定函數是否被調用,傳參是否正確;組件狀態是否發生預料之中的修改;某個dom節點是否存在是否符合期望。
例:
wrapper.find('button').simulate('click');
wrapper.find('input').simulate('keyup');
expect(props.onClick).toBeCalled();// onClick方法被調用
expect(props.onClick).not.toBeCalled() // onClick方法沒被調用
3.5 對生命周期的測試
對於
- componentWillMount
- componentWillUpdate
- componentWillReceiveProps
- shouldComponentUpdate
- componentWillUnmount
可以使用 Enzyme 中的 shallow
方法加載組件,例如
it('componentWillMount', () => { sinon.spy(App.prototype, 'componentWillMount'); shallow(<App />); expect(App.prototype.componentWillMount.calledOnce).toBeTruthy(); });
it('componentWillReceiveProps', () => { let wrapper = shallow(<App role={''} />); sinon.spy(App.prototype, 'componentWillReceiveProps') wrapper.setProps({ role: 'admin' }); expect(App.prototype.componentWillReceiveProps.calledOnce).toBeTruthy(); })
其中,spy 是 sinon 提供的特殊函數,它可以獲取關於函數調用的信息。例如,調用函數的次數、每次調用的參數、返回的值、拋出的錯誤等,可以用來測試一個函數是否被正確地調用。npm i --dave-dev sinon 安裝sinon.
而對於
- componentDidMount
- componentDidUpdate
要用enzyme的mount方法進行加載。
3.6 使用snapshot進行UI測試
import renderer from 'react-test-renderer' it('App -- snapshot', () => { const renderedValue = renderer.create(<App />).toJSON() expect(renderedValue).toMatchSnapshot() })
jest的特色, 快照測試第一次運行的時候會將 React 組件在不同情況下的渲染結果(掛載前)保存一份快照文件。后面每次再運行快照測試時,都會和第一次的比較,diff出兩次快照的變化。
如果需要更新快照文件,使用 npm run test -- -u
命令
3.7 Redux測試
redux官網有詳細的例子,送上傳送門。
4.總結
上面主要介紹了UT的安裝配置及幾個測試demo,以前沒有接觸過單元測試,各種踩坑與啃讀API(jest + enzyme),這些demo基本可以滿足項目中的測試,后續在寫測試中再進步。剛開始接觸測試是一點思路也沒有,看見組件后無從下手,也一直在思考花費這么多時間寫測試到底值不值得,下面是目前遇到的問題和一些思考中的問題,可以一起討論一下:
- 一個好測試的標准,覆蓋率越高就一定越好嗎
- 開發前還是開發后測試
- 怎么測純函數的組件(函數中的const之后總是執行不到)
- error: TypeError: Only absolute URLs are supported 未解決