Jest 测试 React 应用程序
在Facebook,我们使用 Jest 测试 React 应用程序。
安装
使用Create React App
如果你是 React 新手,我们建议使用 Create React App。 它已经包含了可用的 Jest! 你只需要添加 react-test-renderer 来渲染快照。
运行
yarn add --dev react-test-renderer
不使用Create React App
如果你已经有一个应用,你仅需要安装一些包来使他们运行起来。 我们使用babel-jest包和babel-preset-react,从而在测试环境中转换我们代码。 可参考使用babel
运行
yarn add --dev jest babel-jest @babel/preset-env @babel/preset-react react-test-renderer
你的package.json文件应该像下面这样(<current-version>是当前包的最新版本号) 请添加脚本项目和 jest 配置:
// package.json"dependencies": {"react": "<current-version>","react-dom": "<current-version>"},"devDependencies": {"@babel/preset-env": "<current-version>","@babel/preset-react": "<current-version>","babel-jest": "<current-version>","jest": "<current-version>","react-test-renderer": "<current-version>"},"scripts": {"test": "jest"}
// babel.config.jsmodule.exports = {presets: ['@babel/preset-env', '@babel/preset-react'],};
准备工作已经完成!
快照测试
让我们来为一个渲染超链接的 Link 组件创建快照测试
// Link.react.jsimport React from 'react';const STATUS = {HOVERED: 'hovered',NORMAL: 'normal',};export default class Link extends React.Component {constructor(props) {super(props);this._onMouseEnter = this._onMouseEnter.bind(this);this._onMouseLeave = this._onMouseLeave.bind(this);this.state = {class: STATUS.NORMAL,};}_onMouseEnter() {this.setState({class: STATUS.HOVERED});}_onMouseLeave() {this.setState({class: STATUS.NORMAL});}render() {return (<aclassName={this.state.class}href={this.props.page || '#'}onMouseEnter={this._onMouseEnter}onMouseLeave={this._onMouseLeave}>{this.props.children}</a>);}}
现在,使用React的test renderer和Jest的快照特性来和组件交互,获得渲染结果和生成快照文件:
// Link.react.test.jsimport React from 'react';import renderer from 'react-test-renderer';import Link from '../Link.react';test('Link changes the class when hovered', () => {const component = renderer.create(<Link page="http://www.facebook.com">Facebook</Link>,);let tree = component.toJSON();expect(tree).toMatchSnapshot();// manually trigger the callbacktree.props.onMouseEnter();// re-renderingtree = component.toJSON();expect(tree).toMatchSnapshot();// manually trigger the callbacktree.props.onMouseLeave();// re-renderingtree = component.toJSON();expect(tree).toMatchSnapshot();});
当你运行 npm test 或者 jest,将产生一个像下面的文件:
// __tests__/__snapshots__/Link.react.test.js.snapexports[`Link changes the class when hovered 1`] = `<aclassName="normal"href="http://www.facebook.com"onMouseEnter={[Function]}onMouseLeave={[Function]}></a>`;exports[`Link changes the class when hovered 2`] = `<aclassName="hovered"href="http://www.facebook.com"onMouseEnter={[Function]}onMouseLeave={[Function]}></a>`;exports[`Link changes the class when hovered 3`] = `<aclassName="normal"href="http://www.facebook.com"onMouseEnter={[Function]}onMouseLeave={[Function]}></a>`;
下次你运行测试时,渲染的结果将会和之前创建的快照进行比较。快照应与代码更改一起提交。当快照测试失败,你需要去检查是否是你想要或不想要的变动。 如果变动符合预期,你可以通过jest -u调用Jest从而重写存在的快照。
该示例代码在 examples/snapshot
快照测试与 Mocks, Enzyme 和 React 16
在使用 Enzyme 和 React 16+ 时,有一个关于快照测试的警告。如果你使用以下样式模拟模块:
jest.mock('../SomeDirectory/SomeComponent', () => 'SomeComponent');
然后你会在控制台看到警告:
Warning: <SomeComponent /> is using uppercase HTML. Always use lowercase HTML tags in React.# Or:Warning: The tag <SomeComponent> is unrecognized in this browser. If you meant to render a React component, start its name with an uppercase letter.
React 16 由于检查元素类型的方式而触发这些警告,并且模拟模块未通过这些检查。你的选择是:
- 渲染为文本。这样你就不会在快照中看到传递给模拟组件的道具,但它很简单:
jest.mock('./SomeComponent',()=>()=>'SomeComponent'); - 呈现为自定义元素。DOM“自定义元素”不会被检查任何东西,也不应该发出警告。它们是小写的,名称中有一个破折号。
jest.mock('./Widget',()=>()=><mock-widget />); - 使用react-test-renderer. 测试渲染器不关心元素类型,并且很乐意接受例如SomeComponent. 你可以使用测试渲染器检查快照,并使用酶单独检查组件行为。
- 一起禁用警告(应该在你的 jest 设置文件中完成):
jest.mock('fbjs/lib/warning',()=>require('fbjs/lib/emptyFunction'));
这通常不应该是你的选择,因为有用的警告可能会丢失。然而,在某些情况下,例如在测试 react-native 的组件时,我们将 react-native 标签渲染到 DOM 中,许多警告是无关紧要的。另一种选择是调整 console.warn 并抑制特定警告。
DOM测试
如果你想断言和操作你的渲染组件,你可以使用react-testing-library、Enzyme或 React 的TestUtils。以下两个示例使用 react-testing-library 和 Enzyme。
反应测试库
你必须运行yarn add --dev @testing-library/react才能使用 react-testing-library。
让我们实现一个在两个标签之间交换的复选框:
// CheckboxWithLabel.jsimport React from 'react';export default class CheckboxWithLabel extends React.Component {constructor(props) {super(props);this.state = {isChecked: false};// bind manually because React class components don't auto-bind// https://reactjs.org/blog/2015/01/27/react-v0.13.0-beta-1.html#autobindingthis.onChange = this.onChange.bind(this);}onChange() {this.setState({isChecked: !this.state.isChecked});}render() {return (<label><inputtype="checkbox"checked={this.state.isChecked}onChange={this.onChange}/>{this.state.isChecked ? this.props.labelOn : this.props.labelOff}</label>);}}
// __tests__/CheckboxWithLabel-test.jsimport React from 'react';import {cleanup, fireEvent, render} from '@testing-library/react';import CheckboxWithLabel from '../CheckboxWithLabel';// Note: running cleanup afterEach is done automatically for you in @testing-library/react@9.0.0 or higher// unmount and cleanup DOM after the test is finished.afterEach(cleanup);it('CheckboxWithLabel changes the text after click', () => {const {queryByLabelText, getByLabelText} = render(<CheckboxWithLabel labelOn="On" labelOff="Off" />,);expect(queryByLabelText(/off/i)).toBeTruthy();fireEvent.click(getByLabelText(/off/i));expect(queryByLabelText(/on/i)).toBeTruthy();});
这个例子的代码可以在examples/react-testing-library 找到。
Enzyme
你必须运行yarn add --dev enzyme才能使用 Enzyme。如果你使用的是低于 15.5.0 的 React 版本,你还需要安装react-addons-test-utils.
让我们用Enzyme而不是反应测试库从上面重写测试。在这个例子中我们使用了 Enzyme 的浅渲染器。
// __tests__/CheckboxWithLabel-test.jsimport React from 'react';import {shallow} from 'enzyme';import CheckboxWithLabel from '../CheckboxWithLabel';test('CheckboxWithLabel changes the text after click', () => {// Render a checkbox with label in the documentconst checkbox = shallow(<CheckboxWithLabel labelOn="On" labelOff="Off" />);expect(checkbox.text()).toEqual('Off');checkbox.find('input').simulate('change');expect(checkbox.text()).toEqual('On');});
此示例的代码可从examples/enzyme 获得。
自定义转译器
如果你需要更高级的功能,还可以构建自己的变压器。这里没有使用 babel-jest,而是使用 babel 的一个例子:
// custom-transformer.js'use strict';const {transform} = require('@babel/core');const jestPreset = require('babel-preset-jest');module.exports = {process(src, filename) {const result = transform(src, {filename,presets: [jestPreset],});return result ? result.code : src;},};
不要忘记安装@babel/core和babel-preset-jest包以使本示例正常工作。
为了使这个与 Jest 一起工作,你需要更新你的 Jest 配置:"transform": {"\\.js$": "path/to/custom-transformer.js"}。
如果你想建立一个带 babel 支持的转译器,你还可以使用 babel-jest 组合一个并传递选项到你的自定义配置:
const babelJest = require('babel-jest');module.exports = babelJest.createTransformer({presets: ['my-custom-preset'],});