Testing React Apps

在Facebook,我们使用 Jest 测试 React 应用程序。

安装

使用Create React App

如果你是 React 新手,我们建议使用 Create React App。 它已经包含了 可用的 Jest! 您只需要添加 react-test-renderer 来渲染快照。

运行

  1. yarn add --dev react-test-renderer

不使用Create React App

如果你已经有一个应用,你仅需要安装一些包来使他们运行起来。 我们使用babel-jest包和babel-preset-react,从而在测试环境中转换我们代码。 可参考使用babel

运行

  1. yarn add --dev jest babel-jest @babel/preset-env @babel/preset-react react-test-renderer

你的package.json文件应该像下面这样(<current-version>是当前包的最新版本号) 请添加脚本项目和 jest 配置:

  1. // package.json
  2. "dependencies": {
  3. "react": "<current-version>",
  4. "react-dom": "<current-version>"
  5. },
  6. "devDependencies": {
  7. "@babel/preset-env": "<current-version>",
  8. "@babel/preset-react": "<current-version>",
  9. "babel-jest": "<current-version>",
  10. "jest": "<current-version>",
  11. "react-test-renderer": "<current-version>"
  12. },
  13. "scripts": {
  14. "test": "jest"
  15. }
  1. // babel.config.js
  2. module.exports = {
  3. presets: ['@babel/preset-env', '@babel/preset-react'],
  4. };

准备工作已经完成!

快照测试

让我们来为一个渲染超链接的 Link 组件创建快照测试

  1. // Link.react.js
  2. import React from 'react';
  3. const STATUS = {
  4. HOVERED: 'hovered',
  5. NORMAL: 'normal',
  6. };
  7. export default class Link extends React.Component {
  8. constructor(props) {
  9. super(props);
  10. this._onMouseEnter = this._onMouseEnter.bind(this);
  11. this._onMouseLeave = this._onMouseLeave.bind(this);
  12. this.state = {
  13. class: STATUS.NORMAL,
  14. };
  15. }
  16. _onMouseEnter() {
  17. this.setState({class: STATUS.HOVERED});
  18. }
  19. _onMouseLeave() {
  20. this.setState({class: STATUS.NORMAL});
  21. }
  22. render() {
  23. return (
  24. <a
  25. className={this.state.class}
  26. href={this.props.page || '#'}
  27. onMouseEnter={this._onMouseEnter}
  28. onMouseLeave={this._onMouseLeave}
  29. >
  30. {this.props.children}
  31. </a>
  32. );
  33. }
  34. }

现在,使用React的test renderer和Jest的快照特性来和组件交互,获得渲染结果和生成快照文件:

  1. // Link.react.test.js
  2. import React from 'react';
  3. import renderer from 'react-test-renderer';
  4. import Link from '../Link.react';
  5. test('Link changes the class when hovered', () => {
  6. const component = renderer.create(
  7. <Link page="http://www.facebook.com">Facebook</Link>,
  8. );
  9. let tree = component.toJSON();
  10. expect(tree).toMatchSnapshot();
  11. // manually trigger the callback
  12. tree.props.onMouseEnter();
  13. // re-rendering
  14. tree = component.toJSON();
  15. expect(tree).toMatchSnapshot();
  16. // manually trigger the callback
  17. tree.props.onMouseLeave();
  18. // re-rendering
  19. tree = component.toJSON();
  20. expect(tree).toMatchSnapshot();
  21. });

当你运行 npm test 或者 jest,将产生一个像下面的文件:

  1. // __tests__/__snapshots__/Link.react.test.js.snap
  2. exports[`Link changes the class when hovered 1`] = `
  3. <a
  4. className="normal"
  5. href="http://www.facebook.com"
  6. onMouseEnter={[Function]}
  7. onMouseLeave={[Function]}>
  8. Facebook
  9. </a>
  10. `;
  11. exports[`Link changes the class when hovered 2`] = `
  12. <a
  13. className="hovered"
  14. href="http://www.facebook.com"
  15. onMouseEnter={[Function]}
  16. onMouseLeave={[Function]}>
  17. Facebook
  18. </a>
  19. `;
  20. exports[`Link changes the class when hovered 3`] = `
  21. <a
  22. className="normal"
  23. href="http://www.facebook.com"
  24. onMouseEnter={[Function]}
  25. onMouseLeave={[Function]}>
  26. Facebook
  27. </a>
  28. `;

下次你运行测试时,渲染的结果将会和之前创建的快照进行比较。 The snapshot should be committed along with code changes. 当快照测试失败,你需要去检查是否是你想要或不想要的变动。 如果变动符合预期,你可以通过jest -u调用Jest从而重写存在的快照。

该示例代码在 examples/snapshot

快照测试与 Mocks, Enzyme 和 React 16

There’s a caveat around snapshot testing when using Enzyme and React 16+. If you mock out a module using the following style:

  1. jest.mock('../SomeDirectory/SomeComponent', () => 'SomeComponent');

Then you will see warnings in the console:

  1. Warning: <SomeComponent /> is using uppercase HTML. Always use lowercase HTML tags in React.
  2. # Or:
  3. 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 triggers these warnings due to how it checks element types, and the mocked module fails these checks. Your options are:

  1. Render as text. This way you won’t see the props passed to the mock component in the snapshot, but it’s straightforward:

    1. jest.mock('./SomeComponent', () => () => 'SomeComponent');
  2. Render as a custom element. DOM “custom elements” aren’t checked for anything and shouldn’t fire warnings. They are lowercase and have a dash in the name.

    1. jest.mock('./Widget', () => () => <mock-widget />);
  3. Use react-test-renderer. The test renderer doesn’t care about element types and will happily accept e.g. SomeComponent. You could check snapshots using the test renderer, and check component behavior separately using Enzyme.

  4. Disable warnings all together (should be done in your jest setup file):

    1. jest.mock('fbjs/lib/warning', () => require('fbjs/lib/emptyFunction'));

    This shouldn’t normally be your option of choice as useful warnings could be lost. However, in some cases, for example when testing react-native’s components we are rendering react-native tags into the DOM and many warnings are irrelevant. Another option is to swizzle the console.warn and suppress specific warnings.

DOM测试

If you’d like to assert, and manipulate your rendered components you can use react-testing-library, Enzyme, or React’s TestUtils. The following two examples use react-testing-library and Enzyme.

react-testing-library

You have to run yarn add --dev @testing-library/react to use react-testing-library.

Let’s implement a checkbox which swaps between two labels:

  1. // CheckboxWithLabel.js
  2. import React from 'react';
  3. export default class CheckboxWithLabel extends React.Component {
  4. constructor(props) {
  5. super(props);
  6. this.state = {isChecked: false};
  7. // bind manually because React class components don't auto-bind
  8. // https://reactjs.org/blog/2015/01/27/react-v0.13.0-beta-1.html#autobinding
  9. this.onChange = this.onChange.bind(this);
  10. }
  11. onChange() {
  12. this.setState({isChecked: !this.state.isChecked});
  13. }
  14. render() {
  15. return (
  16. <label>
  17. <input
  18. type="checkbox"
  19. checked={this.state.isChecked}
  20. onChange={this.onChange}
  21. />
  22. {this.state.isChecked ? this.props.labelOn : this.props.labelOff}
  23. </label>
  24. );
  25. }
  26. }
  1. // __tests__/CheckboxWithLabel-test.js
  2. import React from 'react';
  3. import {cleanup, fireEvent, render} from '@testing-library/react';
  4. import CheckboxWithLabel from '../CheckboxWithLabel';
  5. // Note: running cleanup afterEach is done automatically for you in @testing-library/react@9.0.0 or higher
  6. // unmount and cleanup DOM after the test is finished.
  7. afterEach(cleanup);
  8. it('CheckboxWithLabel changes the text after click', () => {
  9. const {queryByLabelText, getByLabelText} = render(
  10. <CheckboxWithLabel labelOn="On" labelOff="Off" />,
  11. );
  12. expect(queryByLabelText(/off/i)).toBeTruthy();
  13. fireEvent.click(getByLabelText(/off/i));
  14. expect(queryByLabelText(/on/i)).toBeTruthy();
  15. });

The code for this example is available at examples/react-testing-library.

Enzyme

You have to run yarn add --dev enzyme to use Enzyme. If you are using a React version below 15.5.0, you will also need to install react-addons-test-utils.

Let’s rewrite the test from above using Enzyme instead of react-testing-library. 在这个例子中我们使用了 Enzyme 的浅渲染器

  1. // __tests__/CheckboxWithLabel-test.js
  2. import React from 'react';
  3. import {shallow} from 'enzyme';
  4. import CheckboxWithLabel from '../CheckboxWithLabel';
  5. test('CheckboxWithLabel changes the text after click', () => {
  6. // Render a checkbox with label in the document
  7. const checkbox = shallow(<CheckboxWithLabel labelOn="On" labelOff="Off" />);
  8. expect(checkbox.text()).toEqual('Off');
  9. checkbox.find('input').simulate('change');
  10. expect(checkbox.text()).toEqual('On');
  11. });

The code for this example is available at examples/enzyme.

自定义转译器

If you need more advanced functionality, you can also build your own transformer. Instead of using babel-jest, here is an example of using babel:

  1. // custom-transformer.js
  2. 'use strict';
  3. const {transform} = require('@babel/core');
  4. const jestPreset = require('babel-preset-jest');
  5. module.exports = {
  6. process(src, filename) {
  7. const result = transform(src, {
  8. filename,
  9. presets: [jestPreset],
  10. });
  11. return result ? result.code : src;
  12. },
  13. };

Don’t forget to install the @babel/core and babel-preset-jest packages for this example to work.

为了使这个与 Jest 一起工作,您需要更新您的 Jest 配置:"transform": {"\\.js$": "path/to/custom-transformer.js"}

如果你想建立一个带 babel 支持的转译器,你还可以使用 babel-jest 组合一个并传递选项到您的自定义配置:

  1. const babelJest = require('babel-jest');
  2. module.exports = babelJest.createTransformer({
  3. presets: ['my-custom-preset'],
  4. });