安装
-
create-react-app
创建的应用自带Jest库,执行命令npm run test
就会进入测试单元界面。同时提供强行运行所有单元测试代码、选择只用心满足过滤条件的单元测试用例等高级功能。 - 在项目中,我们配置了相关的参数,具体的配置如下
文件位置,以及目录写法
我们使用单元测试,一般的写法,是在待测试的方法或事组件的同级目录下,声明一个 __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', // 测试环境
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环境, 并可以使用jQuery的方式访问组件的信息 -
render
:静态渲染,它将React组件渲染成静态的HTML字符串,然后使用Cheerio这个库解析这段字符串,并返回一个Cheerio的实例对象,可以用来分析组件的html结构 -
mount
:完全渲染,它将组件渲染加载成一个真实的DOM节点,用来测试DOM API的交互和组件的生命周期。用到了jsdom来模拟浏览器环境
常见方法
-
simulate(event, mock)
:模拟事件,用来触发事件,event
为事件名称,mock
为一个event object
-
instance()
:返回组件的实例 -
find(selector)
:根据选择器查找节点,selector
可以是CSS
中的选择器,或者是组件的构造函数,组件的display name
等 -
at(index)
:返回一个渲染过的对象 -
get(index)
:返回一个react node
,要测试它,需要重新渲染 -
contains(nodeOrNodes)
:当前对象是否包含参数重点node
,参数类型为react
对象或对象数组 -
text()
:返回当前组件的文本内容 -
html()
: 返回当前组件的HTML
代码形式 -
props()
:返回根组件的所有属性 -
prop(key)
:返回根组件的指定属性 -
state()
:返回根组件的状态 -
setState(nextState)
:设置根组件的状态 -
setProps(nextProps)
:设置根组件的属性
编写测试用例
it
每个测试用例一个it函数代表,当然it还有一个别名,叫做test
。
我们先看一个例子:
// ,第一个参数描述它的预期行为
it('当我们传入的参数都是数字的时候', () = > {
// 增加断言语句
})
- 第一个参数:用来表示我们预期这个测试用例的行为
- 第二个参数:是我们实际进行测试的逻辑代码书写的位置
describe
这个是一个测试的套件,在这个里面我们可以写多个it
函数,这个的主要作用就是,我们有的时候,可能也需要对测试用例进行分类,这个时候,我们就用到了describe
。我们看一下他的具体的写法
describe('这是一个小的逻辑单元', () => {
it('当我们传入的参数都是数字的时候', () => {
})
it('当我们传入的参数都是英文的时候', () => {
})
})
-
beforeAll
在开始测试套件之前执行一次(在beforeEach
之前) -
afterAll
在结束测试套件中所有测试用例之后执行一次在(afterEach
之后) -
beforeEach
每个测试用例在执行之前都执行一次 -
afterEach
每个测试用例在执行之后都执行一次
常见的断言
-
expect(value)
:要测试一个值进行断言的时候,要使用expect
对值进行包裹 -
toBe(value)
:使用Object.is
来进行比较,如果进行浮点数的比较,要使用toBeCloseTo
-
not
:用来取反 -
toEqual(value)
:用于对象的深比较 -
toMatch(regexpOrString)
:用来检查字符串是否匹配,可以是正则表达式或者字符串 -
toContain(item)
:用来判断item
是否在一个数组中,也可以用于字符串的判断 -
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
被调用的次数 -
assertions(number)
:验证在一个测试用例中有number
个断言被调用 -
extend(matchers)
:自定义一些断言
实战
对于方法的测试用例的编写
//bytesCount: 获取字符串长度,英文占一个字符,中文占两个字符
import { bytesCount } from '../bytesCount';
describe('测试计算字符串长度', () => {
it('当传入的字符串为汉字', () => {
expect(bytesCount('哈哈哈哈')).toBe(8);
});
it('当传入的字符串为数字', () => {
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('当传入的字符串为数组', () => {
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
的时候,找不到节点 - 我们使用
react
框架,在通过节点进行取值的时候,由于是虚拟dom
,会拿不到节点 - 我们为了简化路径,在
webpack
中配置了公共的路径,但是在在单元测试中,引入在webpack
中配置的路径,会有问题。