安裝
-
create-react-app創(chuàng)建的應用自帶Jest庫,執(zhí)行命令npm run test就會進入測試單元界面。同時提供強行運行所有單元測試代碼、選擇只用心滿足過濾條件的單元測試用例等高級功能。 - 在項目中,我們配置了相關的參數(shù),具體的配置如下
文件位置,以及目錄寫法
我們使用單元測試,一般的寫法,是在待測試的方法或事組件的同級目錄下,聲明一個 __test__的文件夾,并在該文件夾下,命名一個以.test.js結尾的文件。
jest.config.js
module.exports = {
collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts'],
coverageDirectory: '<rootDir>/.tmp/coverage',
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: -10
}
},
moduleNameMapper: {
'^react-native$': 'react-native-web',
'^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy'
}, // 代表需要被Mock的資源名稱
moduleFileExtensions: [
'web.js',
'js',
'web.ts',
'ts',
'web.tsx',
'tsx',
'json',
'web.jsx',
'jsx',
'node'
], // 代表支持加載的文件名
resolver: 'jest-pnp-resolver',
setupFiles: ['react-app-polyfill/jsdom'],
// 配置`setupTests.js`中`enzyme`的連接,使得`enzyme`配置生效
setupTestFrameworkScriptFile: '<rootDir>/src/setupTests.js',
testMatch: [
'<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
'<rootDir>/src/**/?(*.)(spec|test).{js,jsx,ts,tsx}'
], // 配置找到_test_下面的以Component.test.js,Component.spec.js
testEnvironment: 'jsdom', // 測試環(huán)境
testURL: 'http://localhost', // 它反映在諸如location.href之類的屬性中
transform: {
'^.+\\.(js|jsx|ts|tsx)$': '<rootDir>/node_modules/babel-jest',
'^.+\\.css$': '<rootDir>/config/jest/cssTransform.js',
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)':
'<rootDir>/config/jest/fileTransform.js'
},
transformIgnorePatterns: [
'[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$',
'^.+\\.module\\.(css|sass|scss)$'
],
verbose: false
};
setUpTest.js
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });
package.json的啟動腳本配置
"scripts": {
"test": "node scripts/test.js",
},
script下面test.js
'use strict';
// Do this as the first thing so that any code reading it knows the right env.
process.env.BABEL_ENV = 'test';
process.env.NODE_ENV = 'test';
process.env.PUBLIC_URL = '';
// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on('unhandledRejection', err => {
throw err;
});
// Ensure environment variables are read.
require('../config/env');
const { execSync } = require('child_process');
const jest = require('jest');
const argv = process.argv.slice(2);
function isInGitRepository() {
try {
execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
return true;
} catch (e) {
return false;
}
}
function isInMercurialRepository() {
try {
execSync('hg --cwd . root', { stdio: 'ignore' });
return true;
} catch (e) {
return false;
}
}
// Watch unless on CI, in coverage mode, or explicitly running all tests
if (!process.env.CI && argv.indexOf('--coverage') === -1 && argv.indexOf('--watchAll') === -1) {
// https://github.com/facebook/create-react-app/issues/5210
const hasSourceControl = isInGitRepository() || isInMercurialRepository();
argv.push(hasSourceControl ? '--watch' : '--watchAll');
}
jest.run(argv);
enzyme
三種渲染方式
-
shallow:淺渲染,是對官方的Shallow Renderer的封裝。將組件渲染成虛擬DOM對象,只會渲染第一層,子組件將不會被渲染出來,使得效率非常高。不需要DOM環(huán)境, 并可以使用jQuery的方式訪問組件的信息 -
render:靜態(tài)渲染,它將React組件渲染成靜態(tài)的HTML字符串,然后使用Cheerio這個庫解析這段字符串,并返回一個Cheerio的實例對象,可以用來分析組件的html結構 -
mount:完全渲染,它將組件渲染加載成一個真實的DOM節(jié)點,用來測試DOM API的交互和組件的生命周期。用到了jsdom來模擬瀏覽器環(huán)境
常見方法
-
simulate(event, mock):模擬事件,用來觸發(fā)事件,event為事件名稱,mock為一個event object -
instance():返回組件的實例 -
find(selector):根據(jù)選擇器查找節(jié)點,selector可以是CSS中的選擇器,或者是組件的構造函數(shù),組件的display name等 -
at(index):返回一個渲染過的對象 -
get(index):返回一個react node,要測試它,需要重新渲染 -
contains(nodeOrNodes):當前對象是否包含參數(shù)重點node,參數(shù)類型為react對象或對象數(shù)組 -
text():返回當前組件的文本內容 -
html(): 返回當前組件的HTML代碼形式 -
props():返回根組件的所有屬性 -
prop(key):返回根組件的指定屬性 -
state():返回根組件的狀態(tài) -
setState(nextState):設置根組件的狀態(tài) -
setProps(nextProps):設置根組件的屬性
編寫測試用例
it
每個測試用例一個it函數(shù)代表,當然it還有一個別名,叫做test。
我們先看一個例子:
// ,第一個參數(shù)描述它的預期行為
it('當我們傳入的參數(shù)都是數(shù)字的時候', () = > {
// 增加斷言語句
})
- 第一個參數(shù):用來表示我們預期這個測試用例的行為
- 第二個參數(shù):是我們實際進行測試的邏輯代碼書寫的位置
describe
這個是一個測試的套件,在這個里面我們可以寫多個it函數(shù),這個的主要作用就是,我們有的時候,可能也需要對測試用例進行分類,這個時候,我們就用到了describe。我們看一下他的具體的寫法
describe('這是一個小的邏輯單元', () => {
it('當我們傳入的參數(shù)都是數(shù)字的時候', () => {
})
it('當我們傳入的參數(shù)都是英文的時候', () => {
})
})
-
beforeAll在開始測試套件之前執(zhí)行一次(在beforeEach之前) -
afterAll在結束測試套件中所有測試用例之后執(zhí)行一次在(afterEach之后) -
beforeEach每個測試用例在執(zhí)行之前都執(zhí)行一次 -
afterEach每個測試用例在執(zhí)行之后都執(zhí)行一次
常見的斷言
-
expect(value):要測試一個值進行斷言的時候,要使用expect對值進行包裹 -
toBe(value):使用Object.is來進行比較,如果進行浮點數(shù)的比較,要使用toBeCloseTo -
not:用來取反 -
toEqual(value):用于對象的深比較 -
toMatch(regexpOrString):用來檢查字符串是否匹配,可以是正則表達式或者字符串 -
toContain(item):用來判斷item是否在一個數(shù)組中,也可以用于字符串的判斷 -
toBeNull(value):只匹配null -
toBeUndefined(value):只匹配undefined -
toBeDefined(value):與toBeUndefined相反 -
toBeTruthy(value):匹配任何使if語句為真的值 -
toBeFalsy(value):匹配任何使if語句為假的值 -
toBeGreaterThan(number): 大于 -
toBeGreaterThanOrEqual(number):大于等于 -
toBeLessThan(number):小于 -
toBeLessThanOrEqual(number):小于等于 -
toBeInstanceOf(class):判斷是不是class的實例 -
anything(value):匹配除了null和undefined以外的所有值 -
resolves:用來取出promise為fulfilled時包裹的值,支持鏈式調用 -
rejects:用來取出promise為rejected時包裹的值,支持鏈式調用 -
toHaveBeenCalled():用來判斷mock function是否被調用過 -
toHaveBeenCalledTimes(number):用來判斷mock function被調用的次數(shù) -
assertions(number):驗證在一個測試用例中有number個斷言被調用 -
extend(matchers):自定義一些斷言
實戰(zhàn)
對于方法的測試用例的編寫
//bytesCount: 獲取字符串長度,英文占一個字符,中文占兩個字符
import { bytesCount } from '../bytesCount';
describe('測試計算字符串長度', () => {
it('當傳入的字符串為漢字', () => {
expect(bytesCount('哈哈哈哈')).toBe(8);
});
it('當傳入的字符串為數(shù)字', () => {
expect(bytesCount(123)).toBe(3);
});
it('當傳入的字符串為英文', () => {
expect(bytesCount('abd')).toBe(3);
});
it('當傳入的字符串為特殊字符', () => {
expect(bytesCount('~|@%&*')).toBe(6);
});
it('當傳入的字符串為null', () => {
expect(bytesCount(null)).toBe(0);
});
it('當傳入的字符串為undefined', () => {
expect(bytesCount(undefined)).toBe(0);
});
it('當傳入的字符串為數(shù)組', () => {
expect(bytesCount([])).toBe(0);
});
});
對于組件的測試用例的書寫
//ArInput: 組件的作用,是用來在輸入框中輸入,當檢測到,或是換行的時候,會在輸入框下生成一個標簽
import React from 'react';
import { mount, shallow } from 'enzyme';
import ArInput from '../index';
let wrapper;
const props = {
max: 5,
callback: jest.fn(),
tagsChange: jest.fn(),
placeholder: '非英文輸入',
tagsInit: [123, 333]
};
describe('Test MonthPicker Component', () => {
beforeEach(() => {
wrapper = mount(<ArInput {...props} />);
});
it('初始化生成的標簽是否正常', () => {
expect(wrapper.find('.ant-tag').length).toEqual(2);
});
it('placeHolder顯示是否正常', () => {
expect(wrapper.find('input').getDOMNode().placeholder).toEqual('非英文輸入');
});
it('輸入事件是否可以正確的使用', () => {
const mockEvent = {
target: {
value: '13,'
}
};
wrapper.find('input').simulate('keyup', mockEvent);
expect(props.callback).toHaveBeenCalledWith('13,');
expect(wrapper.state('tags').length).toEqual(3);
expect(wrapper.state('tags')[2]).toEqual('13');
});
});
踩過的坑
- 在項目中引入了
antdesign,所有我們在進行配置的時候,渲染的方式只能選用mount,這樣的話,渲染的會有一點慢 - 我們使用了
css-module的形式來編寫樣式,這會導致我們在通過類名查找dom的時候,找不到節(jié)點 - 我們使用
react框架,在通過節(jié)點進行取值的時候,由于是虛擬dom,會拿不到節(jié)點 - 我們?yōu)榱撕喕窂?,?code>webpack中配置了公共的路徑,但是在在單元測試中,引入在
webpack中配置的路徑,會有問題。