写在前面的话
- ~react-testing-library~ 个人喜欢 react-testing-library,但是一般大家使用Enzyme。我会先写一个enzyme的例子,后面的例子使用react-testing-library来写。
- ~示例大纲~ 示例遵循的模板:首先展示React组件,再附上测试和解说。也可以看开头repo的链接。
- ~配置项~ 默认使用
create-react-app作为脚手架搭建了示例基本结构,省略了手动配置的步骤。 - ~Sinon, mocha, chai~ Jest中已经默认提供了很多sinon的方法,所以无需引入sinon。Mocha 和 chai 与jest功能类似,可以替换。Jest的预先配置功能可以与咱们的项目良好配合,所以无需Mocha和chai。
- ~组件命名的格式~ 本文命名React组件的格式是
<TestSomething /> - ~
npm testand jest watch mode~yarn test可用的,npm test在jest watch mode中会失效。 - ~testing a single file~
yarn test+filename - ~React Hooks vs Classes~ 本文中多用React hooks,由于
react-testing-library的强大功能,class 可以和 React Hooks一起用。
有了以上这些信息,我们来看代码吧!
Enzyme
~安装~
npm install enzyme enzyme-to-json enzyme-adapter-react-16
~引入依赖项~
import React from 'react';
import ReactDOM from 'react-dom';
import Basic from '../basic_test';
import Enzyme, { shallow, render, mount } from 'enzyme';
import toJson from 'enzyme-to-json';
import Adapter from 'enzyme-adapter-react-16';
Enzyme.configure({ adapter: new Adapter() })- 我们先引入最基本的项(前三个),React和组件。
- 接着我们引入
Enzyme、从enzyme-to-json库中引入toJson函数。toJson函数能够把shallow 渲染的组件转成json然后保存到snapshot file中 。 - 最后我们引入
Adapter,它帮助enzyme与react v16协作起来,然后进行初始化。
~react-test-renderer~
react自带的测试库,能够替代enzyme 。react-test-renderer的语法如下👇
import TestRenderer from 'react-test-renderer';
import ShallowRenderer from 'react-test-renderer/shallow';
Basic Test with React-test-renderer
it('renders correctly react-test-renderer', () => {
const renderer = new ShallowRenderer();
renderer.render(<Basic />);
const result = renderer.getRenderOutput();
expect(result).toMatchSnapshot();
});
react-test-renderer也认为enzyme在语法上更好一些,ps:它俩做一样的事情。
SnapShot Testing
来看一个 snapshot 测试,文件名为basic_test.test.jsx。
// basic_test.test.jsx
it('renders correctly enzyme', () => {
const wrapper = shallow(<Basic />)
expect(toJson(wrapper)).toMatchSnapshot();
});
在运行这句命令之前,__snapshots__ 文件夹和 test.js.snap 文件已经被自动创建了。每次新的snapshot会与之前的snapshot进行对比,如果snapshot没有变化测试通过,否则不通过。
所以本质上说,snapshot测试通过一行一行地对比组件的变化,这些变化被称为diff
👇来看一个基本的react组件,文件名为basic_test.jsx。
// basic_test.jsx
import React from 'react';
const Basic = () => {
return (
<div >
<h1> Basic Test</h1>
<p> This is a basic Test Component</p>
</div>
);
}
export default Basic;
basic_test.jsx 进行basic_test.test.jsx 的snapshot测试,会生成下面的React DOM 节点🌲
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly enzyme 1`] = `
<div>
<h1>
Basic Test
</h1>
<p>
This is a basic Test Component
</p>
</div>
`;
并且控制台输出
如果修改basic_test.jsx组件如下👇
import React from 'react';
const Basic = () => {
return (
<div >
<h1> Basic Test</h1>
</div>
);
}
export default Basic;snapshot会失败
还会显示Diff内容, -代表这一行被删掉了
这时,w开启activate watch mode 然后 点击u更新 snapshot:snapshot文件会自动更新,测试即可通过~
~snapshot test 反思~
如果你看了上一章节,便可知道我不建议写snapshot测试,上面介绍snapshot是因为它在Enzyme中很普遍,下面我还会解释为何不用这种测试方法。
让我们来回顾一下snapshot测试,它显示了组件与之前有何变化,它的好处是:
- 写snapshot测试很简单,有时只需要简单的几行代码。
- 能够看出组件渲染的结果,用
.debug()清楚地看DOM节点。
~为什么不建议用snapshot testing~
- 这种测试方法只告诉你code的语法有哪些变更。
- 所以它到底测试了什么呢?
- 因为测试时渲染了React的APP,所以他也会测试一部分第三方库的功能。
- Diff也是Git的功能,所以snapshot 测试Diff不是很必要了
- 失败的snapshot测试并不说明功能一定出错,它代表视图是否有了变化。报错后执行了
u更新snapshot后,开发者也不会仔细去看每个Diff的细节。 - snapshot testing还能显示语法的错误,可我们在开发时控制台就能调试语法错误。所以这个功能也没有太大的意义了。
- 许多人的
snapshot测试使用的方法是shallow rendering,这种不渲染子组件的测试结果没有让开发者看到深层次的问题。
使用Enayme,测试功能的细节
下面我来举个反例,说明为何不对功能具体实现的细节进行测试。假如,我们有个简单的计数器组件
import React, { Component } from 'react';
class Counter extends Component {
constructor(props) {
super(props)
this.state = {
count: 0
}
}
increment = () => {
this.setState({count: this.state.count + 1})
}
//This incorrect code will still cause tests to pass
// <button onClick={this.incremen}>
// Clicked: {this.state.count}
// </button>
render() {
return (
<div>
<button className="counter-button" onClick={this.incremen}>
Clicked: {this.state.count}
</button>
</div>
)}
}
export default Counter;
你能注意到吧,上面有一条注释,即使函数名拼错了,但这个测试依然能够通过。
👇下面我们用test来解释原因。
import React from 'react';
import ReactDOM from 'react-dom';
import Counter from '../counter';
import Enzyme, { shallow, render, mount } from 'enzyme';
import toJson from 'enzyme-to-json';
import Adapter from 'enzyme-adapter-react-16';
Enzyme.configure({ adapter: new Adapter() })
// incorrect function assignment in the onClick method
// will still pass the tests.
test('the increment method increments count', () => {
const wrapper = mount(<Counter />)
expect(wrapper.instance().state.count).toBe(0)
// wrapper.find('button.counter-button').simulate('click')
// wrapper.setState({count: 1})
wrapper.instance().increment()
expect(wrapper.instance().state.count).toBe(1)
})
上面这个例子虽然通过了测试,但我没有信心,当用户使用APP的时候是没有问题的。
每当我们改了函数名、CSS类名的时候我们需要重写测试,这种体验太糟糕了,怎么解决呢?
React-testing-library
~useState~
测试越接近用户的行为,你会对测试的结果越有信心。
—— react-testing-library docs
接下来的测试,我们也遵循这个原则,“不断接近用户的行为编写测试”。
👇下面用React Hooks组件来测试state和props。
import React, { useState } from 'react';
const TestHook = (props) => {
const [state, setState] = useState("Initial State")
const changeState = () => {
setState("Initial State Changed")
}
const changeNameToSteve = () => {
props.changeName()
}
return (
<div>
<button onClick={changeState}>
State Change Button
</button>
<p>{state}</p>
<button onClick={changeNameToSteve}>
Change Name
</button>
<p>{props.name}</p>
</div>
)
}
export default TestHook;
props来自父组件
const App = () => {
const [state, setState] = useState("Some Text")
const [name, setName] = useState("Moe")
...
const changeName = () => {
setName("Steve")
}
return (
<div className="App">
<Basic />
<h1> Counter </h1>
<Counter />
<h1> Basic Hook useState </h1>
<TestHook name={name} changeName={changeName}/>
...
现在回想一下我们的测试原则:“模仿用户的行为”,然后,我们的测试怎么写呢?
设想一下用户的交互过程:看到了UI上的文字,看到了按钮的文字,点击按钮,UI上面文字更新了。
我们用React-testing-library测试上面的例子。
~安装~
npm install @testing-library/react
而不是执行 npm install react-testing-library
~测试例子解读~
import React from 'react';
import ReactDOM from 'react-dom';
import TestHook from '../test_hook.js';
import {render, fireEvent, cleanup} from '@testing-library/react';
import App from '../../../App'
afterEach(cleanup)
it('Text in state is changed when button clicked', () => {
const { getByText } = render(<TestHook />);
expect(getByText(/Initial/i).textContent).toBe("Initial State")
fireEvent.click(getByText("State Change Button"))
expect(getByText(/Initial/i).textContent).toBe("Initial State Changed")
})
it('button click changes props', () => {
const { getByText } = render(<App>
<TestHook />
</App>)
expect(getByText(/Moe/i).textContent).toBe("Moe")
fireEvent.click(getByText("Change Name"))
expect(getByText(/Steve/i).textContent).toBe("Steve")
})
- 引入依赖项
afterEach(cleanup)每个test之后我们需要unmount或者cleanup,因为我们选择的不是shallow render。getByText是我们从render函数得到的 object中解构出来的查询函数 。虽然还有其他的类似查询函数,但它用得最多。- 在测试state的时候,我们没用任何函数名或者state变量的名字。时刻记着“模仿用户行为,不测试实现细节”的原则。用户看到UI的文字,我们会去检索DOM节点的文字,我们也会用类似的方式检索按钮并点击它。再检索文字的变化。
-
(/Initial/i)是正则表达式,它返回第一个包含Initial的元素。 - 对于
props的测试方法也类似。当props要在 App.js 中改变时,组件要一起渲染出来。同时,注意用👆“模仿用户行为”的原则写测试。
希望上面介绍的方法能帮助你学会用React-testing-library和“测试原则”写测试,下一篇文章我们来看看一些例外的情况。
感谢阅读