在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 配置: 请添加脚本项目和 jest 配置:

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

准备工作已经完成!

快照测试

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

  1. // Link.react.js
  2. import React, {useState} from 'react';
  3. const STATUS = {
  4. HOVERED: 'hovered',
  5. NORMAL: 'normal',
  6. };
  7. const Link = ({page, children}) => {
  8. const [status, setStatus] = useState(STATUS.NORMAL);
  9. const onMouseEnter = () => {
  10. setStatus(STATUS.HOVERED);
  11. };
  12. const onMouseLeave = () => {
  13. setStatus(STATUS.NORMAL);
  14. };
  15. return (
  16. <a
  17. className={status}
  18. href={page || '#'}
  19. onMouseEnter={onMouseEnter}
  20. onMouseLeave={onMouseLeave}
  21. >
  22. {children}
  23. </a>
  24. );
  25. };
  26. export default Link;

注意:示例只展现了函数式组件,其实Class组件也是一样的。 相关区别请查看:React: Function and Class Components. 注意 ,我们希望对于Class组件,Jest应该更多的去测试Props而不是Class中的函数。

现在,使用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. `;

下次你运行测试时,渲染的结果将会和之前创建的快照进行比较。 代码变动时,快照也应该被提交。 当快照测试失败,你需要去检查是否是你想要或不想要的变动。 如果变动符合预期,你可以通过jest -u调用Jest从而重写存在的快照。

该示例代码在 examples/snapshot

快照测试与 Mocks, Enzyme 和 React 16

快照测试在 Enzyme 和 React 16+ 中使用时有一个注意事项。 如果您使用以下方式模拟模块:

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

然后您将在控制台中看到警告:

  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. Always use lowercase HTML tags in React.
  4. # Or:
  5. 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 触发这些警告,取决于它是如何检查元素类型的,在这些检查中模拟模块会失败。 您可以:

  1. 渲染为纯文本。 这种方式你看不到传递给模拟组件的Props,但最直观。

jest.mock(‘./SomeComponent’, () => () => ‘SomeComponent’);

  1. ```
  1. 渲染为自定义元素。 DOM的“自定义元素“不会检查任何属性,所以也不会触发warnings。 他们都是小写的、中划线分割的单词。

    1. jest.mock('./Widget', () => () => <mock-widget />);
  2. 使用 react-test-renderer。 这个test渲染器不校验元素类型,并乐意接受SomeComponent作为参数。 你可以使用react-test-renderer进行快照检测,使用单独使用Enzyme进行组件行为监测。

  3. 禁用所有警告(在jest setup file中):

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

    这是下策,因为所有有用的警告也都会丢失。 不过,依然有其适用场景。例如我们需要测试 react-native的组件编译为DOM,很多警告是无关紧要的。 另一个方式是使用控制台忽略特定的警告信息。

DOM测试

如果你想断言或者操纵你的渲染组件,你可以使用react-testing-library, Enzyme, 或者React的 TestUtils。 接下来我们讲两个 react-testing-library 和Enzyme的例子。

react-testing-library

你需要运行 yarn add --dev @testing-library/react才能使用 react-testing-library。

这里我们实现一个在两个标签之间切换的复选框。

  1. // CheckboxWithLabel.js
  2. import React, {useState} from 'react';
  3. const CheckboxWithLabel = ({labelOn, labelOff}) => {
  4. const [isChecked, setIsChecked] = useState(false);
  5. const onChange = () => {
  6. setIsChecked(!isChecked);
  7. };
  8. return (
  9. <label>
  10. <input type="checkbox" checked={isChecked} onChange={onChange} />
  11. {isChecked ? labelOn : labelOff}
  12. </label>
  13. );
  14. };
  15. export default CheckboxWithLabel;
  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. });
  16. afterEach(cleanup);
  17. it('CheckboxWithLabel changes the text after click', () => {
  18. const {queryByLabelText, getByLabelText} = render(
  19. <CheckboxWithLabel labelOn="On" labelOff="Off" />,
  20. );
  21. expect(queryByLabelText(/off/i)).toBeTruthy();
  22. fireEvent.click(getByLabelText(/off/i));
  23. expect(queryByLabelText(/on/i)).toBeTruthy();
  24. });

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

Enzyme

安装Enzyme 。yarn add --dev enzyme。 如果你的React版本低于15.5.0,则也需要再安装react-addons-test-utils

我们使用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. });

上述示例的源码可以在 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/core:

  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 || src;
  12. },
  13. };

别忘记安装 @babel/core and babel-preset-jest

为了使这个与 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. });

详情见 dedicated docs