Open SunXinFei opened 4 years ago
Jest是FB家的开源的单元测试的工具,对React项目支持非常友好。
jest运行时,如输入值和期望值如果不相符,清晰地标记显示出,方便开发者调试:
如果单元测试用例的组件中有localStorage的使用时,会有localstorage is not defined
的错误或者调用getItem of null
等方法不能使用,
我们建一个setUpTest.js
内容如下:
import 'jsdom-global/register';
// browserMocks.js
const localStorageMock = (() => {
let store = {};
return {
getItem(key) {
return store[key] || null;
},
setItem(key, value) {
store[key] = value.toString();
},
clear() {
store = {};
},
};
})();
global.localStorage = localStorageMock;
然后在jest.config.js
中配置
setupFilesAfterEnv: ['./tests/setupTests.js'],
在jest.config.js
中配置
globals: {
REACT_APP_ENV: 'dev'
},
这一类的配置有点类似,在业务组件的测试文件如Tag.test.js,注意亮点一个是标签为Tag.WrappedComponent,属性传递的即为Tag组件需要的store中的对象
import { shallow } from 'enzyme';
import React from 'react'; //必须要有,防止jsx在测试环境报错
import Tag from './index';
test('Tag test', () => {
const wrapper = shallow(<Tag.WrappedComponent currentUser={{}} crowdList={[]} />);
expect(wrapper.state('activeKey')).toBe('1');
});
import { shallow } from 'enzyme';
import React from 'react'; //必须要有,防止jsx在测试环境报错
import Tag from './index';
test('Tag test', () => {
const wrapper = shallow(<Tag.WrappedComponent currentUser={{}} crowdList={[]} />);
const instance = wrapper.instance();//获取组件的实例
expect(instance.addFn(2)).toEqual(3);
});
render、mount、shallow的区别
enzyme有3种渲染方式:render、mount、shallow; render采用的是第三方库Cheerio的渲染,渲染结果是普通的html结构,对于snapshot使用render比较合适。
shallow和mount对组件的渲染结果不是html的dom树,而是react树,如果你chrome装了react devtool插件,他的渲染结果就是react devtool tab下查看的组件结构,而render函数的结果是element tab下查看的结果。
这些只是渲染结果上的差别,更大的差别是shallow和mount的结果是个被封装的ReactWrapper,可以进行多种操作,譬如find()、parents()、children()等选择器进行元素查找;state()、props()进行数据查找,setState()、setprops()操作数据;simulate()模拟事件触发。
shallow只渲染当前组件,只能能对当前组件做断言;mount会渲染当前组件以及所有子组件,对所有子组件也可以做上述操作。一般交互测试都会关心到子组件,使用的都是mount。但是mount耗时更长,内存占用的更多。
snapshot是使用render方法,并生成一个文件夹,每次运行进行前后的“快照”对比,这里的“快照”要单独说明一下,不是照片,在不toJSON情况下指的是生成的类似于AST(抽象语法树)的json结构,
import { render } from 'enzyme';
import React from 'react'; //必须要有,防止jsx在测试环境报错
import Tag from './index';
test('Tag test', () => {
const wrapper = render(<Tag.WrappedComponent currentUser={{}} crowdList={[]} />);
expect(wrapper).toMatchSnapshot();
});
enzyme默认是支持jsx中css的普通写法的,但是如果组件内部是使用的import的形式,比如:
<div className={styles.add_intersect} onClick={this.addNewGroup}>
新增组
</div>
上面这种情况下,我们使用enzyme的find等选择器是获取不到dom元素的,这里我们需要借助
identity-obj-proxy
:
jest.config.js
中添加配置:
moduleNameMapper: {
'\\.(css|less|sass|scss)$': 'identity-obj-proxy',
},
import { shallow, mount } from 'enzyme';
test('click action', () => {
const wrapper = mount(<Tag.WrappedComponent currentUser={{}} crowdList={[]} />);
wrapper
.find('.add_intersect')
.first()
.simulate('click');
expect(wrapper.state('activeObj')).toEqual({
ext: 'inter',
index: 1,
});
});
参考:
https://github.com/facebook/jest/issues/3094
jest解决
window.matchMedia is not a function
或 antdesign中The above error occurred in the <Row> component
在
setupTests.js
中添加下面代码即可,mock出matchMedia:Object.defineProperty(window, 'matchMedia', { writable: true, value: jest.fn().mockImplementation(query => ({ matches: false, media: query, onchange: null, addListener: jest.fn(), // deprecated removeListener: jest.fn(), // deprecated addEventListener: jest.fn(), removeEventListener: jest.fn(), dispatchEvent: jest.fn(), })), });
参考
https://github.com/ant-design/ant-design/issues/21096
jest解决alias路径问题
module.exports = { moduleNameMapper: { "^@/(.*)$": "<rootDir>/src/$1" } }
将这个配置进行添加,这样jest中就可以支持
@/utils/utils.js
jest解决
Attempted to log "Warning: An update to ForwardRef(TabNavList) inside a test was not wrapped in act(...).``This ensures that you're testing the behavior the user would see in the browser.
在
setupTests.js
中添加如下代码const mockConsoleMethod = (realConsoleMethod) => { const ignoredMessages = [ 'test was not wrapped in act(...)', ]
return (message, ...args) => { const containsIgnoredMessage = ignoredMessages.some(ignoredMessage => message.includes(ignoredMessage))
if (!containsIgnoredMessage) {
realConsoleMethod(message, ...args)
}
} }
console.warn = jest.fn(mockConsoleMethod(console.warn)) console.error = jest.fn(mockConsoleMethod(console.error))
参考:
https://github.com/enzymejs/enzyme/issues/2073
## jest解决puppeteer安装失败导致`Browser is not downloaded`的问题
在`jest-puppeteer.config.js`中添加如下配置,将运行的browser指向本机的chrome浏览器
launch: { executablePath: '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome', }
import ElementUI from 'element-ui';
import { shallowMount, createLocalVue } from "@vue/test-utils";
import Rules from "@/views/rules.vue";
import Cases2WashSaveData from "./cases/case-washSaveData.js";
const localVue = createLocalVue() localVue.use(ElementUI)
test("washSaveData", () => { Cases2WashSaveData.forEach((caseData) => { const wrapper = shallowMount(Rules,{localVue}, {}); expect(wrapper.vm.washSaveData(caseData.test)).toStrictEqual(caseData.expect); }); });
- `npm i --save-dev identity-obj-proxy` 安装
- jest.config.js添加样式代理
```js
module.exports = {
preset: "@vue/cli-plugin-unit-jest",
moduleNameMapper: {
'\\.(css|less|sass|scss)$': 'identity-obj-proxy',
},
};
stubs: ['router-link']
const wrapper = shallowMount(Rules,{localVue, stubs: ['router-link']}, {});
参考: https://stackoverflow.com/questions/49681546/vue-test-utils-unknown-custom-element-router-link
import { shallowMount, createLocalVue, mount } from "@vue/test-utils";
import Vuex from 'vuex';
const localVue = createLocalVue();
localVue.use(Vuex)
let actions let store
beforeEach(() => { actions = { setMenuExpand: jest.fn() } store = new Vuex.Store({ state: { menuExpand: false, }, actions }) }) /*addNewGroup的click事件 / test("addNewGroup", () => { const wrapper = mount(Rules, { localVue,store, stubs: ["router-link"] }); });
参考:https://vue-test-utils.vuejs.org/zh/guides/#%E5%9C%A8%E7%BB%84%E4%BB%B6%E4%B8%AD%E6%B5%8B%E8%AF%95-vuex
## 相对较全的demo
```js
import ElementUI from "element-ui";
import { shallowMount, createLocalVue, mount } from "@vue/test-utils";
import Rules from "@/views/rules.vue";
import Vuex from "vuex";
const localVue = createLocalVue();
localVue.use(ElementUI);
localVue.use(Vuex);
let actions;
let store;
beforeEach(() => {
actions = {
setMenuExpand: jest.fn(),
};
store = new Vuex.Store({
state: {
menuExpand: false,
},
actions,
});
});
/**addNewGroup的click事件 */
test("addNewGroup", () => {
const wrapper = mount(Rules, { localVue, store, stubs: ["router-link"] });
//初始化state数据
wrapper.setData({
ruleObj: { include: [[]], exclude: [[]] },
});
//寻找Dom
const button = wrapper.find(".add-group");
//Dom文本
expect(button.text()).toBe("Btn");
//事件触发
button.trigger("click");
//state数据
expect(wrapper.vm.ruleObj).toStrictEqual({
include: [[], []],
exclude: [[]],
});
});
好处
单元测试的好处就不用多说了,对于敏捷开发的迭代需求或者业务逻辑的重构,有了单元测试之后非常方便的担保业务逻辑平滑过渡,而且单元测试的case的存在,可以有效的说明逻辑,比代码注释更为清晰。在MVVM框架流行的今天,数据驱动DOM使得单元测试更加重要而且可行性更高。
痛点
前端单元测试推动是一直有痛点的,包括一些大厂对前端单元测试这个态度不是很统一。原因主要概括为两点:
从而导致业务线前期稳定性靠开发者把控,后面项目逐渐庞大,测试用例又没有时间补贴,或者开发者转了战场。
折中
比较好的处理方式是折中方案,Utils工具类中的方法必须进行单元测试,业务基础组件和项目的基础业务逻辑必须进行单元测试,这样可以很好的避免后期基础的逻辑,手动痛苦地回归case。而目前看来,前端很多项目也确实是这么做的。其他的case则视重要性与时间代价视情况而定了。