跳转至主内容
版本:29.7

测试React程序

At Facebook, we use Jest to test React applications.

安装

使用Create React App

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.

运行

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

不使用Create React App

如果你已经有一个应用,你仅需要安装一些包来使他们运行起来。 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.

运行

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

Your package.json should look something like this (where <current-version> is the actual latest version number for the package). 请添加脚本项目和 jest 配置:

{
"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!

快照测试

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>
);
}
备注

示例正在使用函数组件,但类组件可以同样方式测试。 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的test renderer和Jest的快照特性来和组件交互,获得渲染结果和生成快照文件:

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();
});

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>
`;

下次你运行测试时,渲染的结果将会和之前创建的快照进行比较。 代码变动时,快照也应该被提交。 当快照测试失败,你需要去检查是否是你想要或不想要的变动。 If the change is expected you can invoke Jest with jest -u to overwrite the existing snapshot.

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

Snapshot Testing with Mocks, Enzyme and 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 触发这些警告,取决于它是如何检查元素类型的,在这些检查中模拟模块会失败。 您可以:

  1. 渲染为纯文本。 这种方式你看不到传递给模拟组件的Props,但最直观。
    jest.mock('./SomeComponent', () => () => 'SomeComponent');
  2. 渲染为自定义元素。 DOM的“自定义元素“不会检查任何属性,所以也不会触发warnings。 他们都是小写的、中划线分割的单词。
    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. 你可以使用react-test-renderer进行快照检测,使用单独使用Enzyme进行组件行为监测。
  4. 禁用所有警告(在jest setup file中):
    jest.mock('fbjs/lib/warning', () => require('fbjs/lib/emptyFunction'));
    这是下策,因为所有有用的警告也都会丢失。 不过,依然有其适用场景。例如我们需要测试 react-native的组件编译为DOM,很多警告是无关紧要的。 另一个方式是使用控制台忽略特定的警告信息。

DOM测试

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

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

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/[email protected] 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();
});

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

自定义转译器

如果你需要更多高级功能,你也可以自定义你的转译器。 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;
},
};

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

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

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.