因为是从0 到 1 进行搭建组件,首先先创建一个next 的项目。本文只用于记录自动化测试
技术选型为:Jest + Enzyme
测试工程搭建
- 先创建一个react app
npx create-react-app test-with-cra
- 添加enzyme 等包
yarn add enzyme enzyme-adapter-react-16 -D
- 进行相关配置,这里暂时没有生效
- 创建
src/setupTest.js文件
- 文件内容
// src/setupTests.js
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });
现在就可以执行yarn test 进行测试了。
测试 React 组件
我们先写一个简单的js的例子,然后让这个例子运行起来,目录结构如上:
// Button/index.jsx
import React, { useEffect, useState } from "react";
import services from "./services";
import tracker from "./tracker";
const Button = (props) => {
const [buttonName, setBtnName] = useState("buttonDefaultName");
const [data, setData] = useState({});
const getData = async () => {
const res = await services.fetchData();
setData(res);
};
tracker.page("button init");
useEffect(()=>{
getData();
},[])
useEffect(() => {
setTimeout(() => {
setBtnName("buttonAfter3seconds");
}, 3000);
}, []);
const clickBtn = () => {
console.log(data);
props.onButtonClick && props.onButtonClick();
};
return (
<div>
<div className="button" onClick={clickBtn}>{props.buttonName || buttonName}</div>
</div>
);
};
export default Button;
// src/Button/services.js
const fetchData = async ()=>{
// 一个请求,先随便写一个吧
return "mockdata"
}
const services={
fetchData
}
export default services;
// src/Button/tracker.js
const page = (value) => {
// 是一个函数,假设这里是一个埋点函数
console.log(value);
};
const tracker={
page
}
export default tracker;
然后修改一下App.js
import MyButton from'./Button/index';
import './App.css';
function App() {
return (
<div className="App">
<header className="App-header">
<MyButton></MyButton>
</header>
</div>
);
}
export default App;
启动一下,跑起来的页面是这样的:
我们的测试目的会围绕这几个关键点:
- 组件是否符合预期的渲染了?
- 事件点击的回调函数是否正确执行了?
- 业务埋点函数是否被正常调用了?
- 异步接口请求如何校验?
- setimeout 等异步后的操作如何校验?
组件是否符合预期的渲染了
当传入属性不同的时候,可能会导致UI的渲染也是不一样的。
propsA → Component Render → renderResultA
propsB → Component Render → renderResultB
让我们看一下
//__test__/button1.test.js
// snapshot
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import React from 'react'
import { shallow } from 'enzyme';
configure({ adapter: new Adapter() });
describe('Button 组件测试', () => {
it('渲染正常', () => {
const Button = require('./Button').default
// enzyme的shallow方法用于渲染组件
const componentWrapper = shallow(<Button />);
expect(componentWrapper.html()).toMatchSnapshot();
});
it('props传入buttonName,渲染正常', () => {
const Button = require('./Button').default
const componentWrapper = shallow(<Button buttonName={'mockButtonName'}/>);
expect(componentWrapper.html()).toMatchSnapshot();
});
});
让我们执行一个 yarn test 这个时候会生成一个文件快照生成不同条件下的结果:
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Button 组件测试 props传入buttonName,渲染正常 1`] = `"<div>mockButtonName</div>"`;
exports[`Button 组件测试 渲染正常 1`] = `"<div>buttonDefaultName</div>"`;
当我们下次改动到了组件的渲染逻辑的时候,snapshot会提醒渲染和之前是不一样的,要是在预期内的改动,可以更新改动,若不是预期内的改动,需要看一下问题出现在哪里。
事件点击的回调函数是否正确执行了
对于一些组件的行为,我们可以通过 Enzyme渲染出的组件进行模拟,并断言结果。
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import React from 'react'
import { shallow } from 'enzyme';
configure({ adapter: new Adapter() });
describe('Button 组件测试', () => {
it('click点击事件测试,mockfn校验', () => {
// 生成一个mockfn,用于校验点击后的props.func是否被调用
const mockClick = jest.fn()
const Button = require('./Button').default
const componentWrapper = shallow(<Button onButtonClick={mockClick}/>);
// 模拟点击事件
componentWrapper.find('.button').at(0).simulate('click')
// 校验mockfn被调用
expect(mockClick).toHaveBeenCalled();
});
});
jest.fn() 用来生成mock函数,可以通过mockClick 这个句柄去判断函数的调用情况
enzyme提供了类似于jquery的dom选择器,并通过 simulate(event) 的方式来模拟事件
业务埋点函数是否被正常调用了
我们可以看到组件通过模块的方式引入了一个 tracker 函数,用于在组件初始化的时候触发埋点行为,那如何验证tracker是否被正常调用呢?我们下面的写法就是用mock的函数替代原来的函数。
//__test__/button1.test.js
// snapshot
import { configure } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import React from "react";
import { shallow } from "enzyme";
configure({ adapter: new Adapter() });
describe("Button 组件测试", () => {
it("校验埋点是否被正常调用", () => {
// 生成一个mock函数
const page = jest.fn();
// 声明 mock tracker这个模块,并在第二个参数传入mock方法
jest.doMock("./Button/tracker", () => {
return { page : page};
});
const Button = require("./Button").default;
shallow(<Button />);
// mock函数被调用,并且参数是 'button init'
expect(page).toHaveBeenCalledWith("button init")
});
});
异步接口请求
类组件的写法如下:
describe('Button 组件测试', () => {
it('接口测试',async () => {
jest.doMock('./Button/services',()=>{
return ()=>{
return 'c'
}
})
const Button = require('./Button').default
const componentWrapper = await shallow(<Button />);
// 通过enzyme渲染的组件可以通过 .state() 方法拿到组件 state 状态
// expect(componentWrapper.state('data')).toEqual('mockdata')
expect(componentWrapper.state("data")).toEqual('mockdata')
});
});
hook 相关的写法参考 :github.yanhaixiang.com/jest-tutori…
setimeout 等异步操作是否按预期执行了
那么如何测试到三秒后的状态呢?在测试中等三秒吗?显然不是,我们可以使用jest 提供的faketimer 来帮我们快进时间
describe('Button 组件测试', () => {
it('3秒后渲染正常', () => {
const Button = require('./Button').default
const componentWrapper = shallow(<Button />);
// 快速执行所有的 macro-task (eg. setTimeout(), setInterval())
jest.runAllTimers()
expect(componentWrapper.html()).toMatchSnapshot();
});
});
自测报告
- Statements 语句覆盖率,它其实对应的就是js语法上的语句,js解析成ast数中类型为statement。
- Branches 分支覆盖率,通俗点理解就是if/else这类条件
- Functions 函数覆盖率
- Lines 行数覆盖率,就是代码执行了多少行
所有断言的枚举:man.hubwiz.com/manual/Jest