Skip to main content
Version: 29.7

测试 React 应用

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

¥At Facebook, we use Jest to test React applications.

设置

¥Setup

使用 Create React App 设置

¥Setup with Create React App

如果你是 React 新手,我们建议使用 创建反应应用。它已准备好使用并且 与 Jest 一起发货!你只需要添加 react-test-renderer 即可渲染快照。

¥If you are new to React, we recommend using Create React App. It is ready to use and ships with Jest! You will only need to add react-test-renderer for rendering snapshots.

运行

¥Run

npm install --save-dev react-test-renderer

不使用 Create React App 进行设置

¥Setup without Create React App

如果你有现有的应用,则需要安装一些软件包以使所有内容都能正常工作。我们使用 babel-jest 包和 react babel 预设来转换测试环境中的代码。另请参阅 使用 Babel

¥If you have an existing application you'll need to install a few packages to make everything work well together. We are using the babel-jest package and the react babel preset to transform our code inside of the test environment. Also see using babel.

运行

¥Run

npm install --save-dev jest babel-jest @babel/preset-env @babel/preset-react react-test-renderer

你的 package.json 应该看起来像这样(其中 <current-version> 是包的实际最新版本号)。请添加脚本和 jest 配置条目:

¥Your package.json should look something like this (where <current-version> is the actual latest version number for the package). Please add the scripts and jest configuration entries:

{
"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.js
module.exports = {
presets: [
'@babel/preset-env',
['@babel/preset-react', {runtime: 'automatic'}],
],
};

你就可以出发了!

¥And you're good to go!

快照测试

¥Snapshot Testing

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

¥Let's create a snapshot test for a Link component that renders hyperlinks:

Link.js
import {useState} from 'react';

const STATUS = {
HOVERED: 'hovered',
NORMAL: 'normal',
};

export default function Link({page, children}) {
const [status, setStatus] = useState(STATUS.NORMAL);

const onMouseEnter = () => {
setStatus(STATUS.HOVERED);
};

const onMouseLeave = () => {
setStatus(STATUS.NORMAL);
};

return (
<a
className={status}
href={page || '#'}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{children}
</a>
);
}
注意

示例是使用函数组件,但类组件可以以相同的方式进行测试。参见 React:函数和类组件。提醒一下,对于 Class 组件,我们希望使用 Jest 来直接测试 props 而不是方法。

¥Examples are using Function components, but Class components can be tested in the same way. See React: Function and Class Components. Reminders that with Class components, we expect Jest to be used to test props and not methods directly.

现在让我们使用 React 的测试渲染器和 Jest 的快照功能与组件交互并捕获渲染的输出并创建快照文件:

¥Now let's use React's test renderer and Jest's snapshot feature to interact with the component and capture the rendered output and create a snapshot file:

Link.test.js
import renderer from 'react-test-renderer';
import Link from '../Link';

it('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 callback
renderer.act(() => {
tree.props.onMouseEnter();
});
// re-rendering
tree = component.toJSON();
expect(tree).toMatchSnapshot();

// manually trigger the callback
renderer.act(() => {
tree.props.onMouseLeave();
});
// re-rendering
tree = component.toJSON();
expect(tree).toMatchSnapshot();
});

当你运行 yarn testjest 时,将生成如下输出文件:

¥When you run yarn test or jest, this will produce an output file like this:

__tests__/__snapshots__/Link.test.js.snap
exports[`changes the class when hovered 1`] = `
<a
className="normal"
href="http://www.facebook.com"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Facebook
</a>
`;

exports[`changes the class when hovered 2`] = `
<a
className="hovered"
href="http://www.facebook.com"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Facebook
</a>
`;

exports[`changes the class when hovered 3`] = `
<a
className="normal"
href="http://www.facebook.com"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Facebook
</a>
`;

下次运行测试时,渲染的输出将与之前创建的快照进行比较。快照应与代码更改一起提交。当快照测试失败时,你需要检查它是有意的还是无意的更改。如果需要进行更改,你可以使用 jest -u 调用 Jest 来覆盖现有快照。

¥The next time you run the tests, the rendered output will be compared to the previously created snapshot. The snapshot should be committed along with code changes. When a snapshot test fails, you need to inspect whether it is an intended or unintended change. If the change is expected you can invoke Jest with jest -u to overwrite the existing snapshot.

此示例的代码可在 examples/snapshot 处获取。

¥The code for this example is available at examples/snapshot.

使用 Mocks、Enzyme 和 React 16+ 进行快照测试

¥Snapshot Testing with Mocks, Enzyme and React 16+

使用 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:

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

然后你会在控制台看到警告:

¥Then you will see warnings in the console:

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 由于检查元素类型的方式而触发这些警告,并且模拟模块未通过这些检查。你的选择是:

¥React 16 triggers these warnings due to how it checks element types, and the mocked module fails these checks. Your options are:

  1. 渲染为文本。这样你就不会在快照中看到传递给模拟组件的 props,但它很简单:

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

    jest.mock('./SomeComponent', () => () => 'SomeComponent');
  2. 渲染为自定义元素。DOM "自定义元素" 不会检查任何内容,因此不应触发警告。它们是小写字母,名称中带有破折号。

    ¥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.

    jest.mock('./Widget', () => () => <mock-widget />);
  3. 使用 react-test-renderer。测试渲染器不关心元素类型,并且会很乐意接受例如 SomeComponent。你可以使用测试渲染器检查快照,并使用 Enzyme 单独检查组件行为。

    ¥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. 一起禁用警告(应该在你的 jest 设置文件中完成):

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

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

    这通常不应该是你的选择,因为有用的警告可能会丢失。然而,在某些情况下,例如在测试 react-native 的组件时,我们将 react-native 标签渲染到 DOM 中,并且许多警告是无关紧要的。另一种选择是调整 console.warn 并抑制特定警告。

    ¥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 测试

¥DOM Testing

如果你想断言并操作渲染的组件,你可以使用 @testing-library/reactEnzyme 或 React 的 TestUtils。以下示例使用 @testing-library/react

¥If you'd like to assert, and manipulate your rendered components you can use @testing-library/react, Enzyme, or React's TestUtils. The following example use @testing-library/react.

@testing-library/react

npm install --save-dev @testing-library/react

让我们实现一个在两个标签之间交换的复选框:

¥Let's implement a checkbox which swaps between two labels:

CheckboxWithLabel.js
import {useState} from 'react';

export default function CheckboxWithLabel({labelOn, labelOff}) {
const [isChecked, setIsChecked] = useState(false);

const onChange = () => {
setIsChecked(!isChecked);
};

return (
<label>
<input type="checkbox" checked={isChecked} onChange={onChange} />
{isChecked ? labelOn : labelOff}
</label>
);
}
__tests__/CheckboxWithLabel-test.js
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 处获取。

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

定制转换器

¥Custom transformers

如果你需要更高级的功能,你还可以构建自己的转换器。下面是使用 @babel/core 的示例,而不是使用 babel-jest

¥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:

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

不要忘记安装 @babel/corebabel-preset-jest 软件包才能使该示例正常工作。

¥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"}

¥To make this work with Jest you need to update your Jest configuration with this: "transform": {"\\.js$": "path/to/custom-transformer.js"}.

如果你想构建一个支持 babel 的转换器,你还可以使用 babel-jest 来编写一个转换器并传入你的自定义配置选项:

¥If you'd like to build a transformer with babel support, you can also use babel-jest to compose one and pass in your custom configuration options:

const babelJest = require('babel-jest');

module.exports = babelJest.createTransformer({
presets: ['my-custom-preset'],
});

详细信息请参见 专用文档

¥See dedicated docs for more details.