跳至主内容
版本:30.0

测试 React 应用

非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

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

环境配置

对于现有项目,您需要安装以下依赖包以确保功能正常。我们使用 babel-jest 包和 react 的 babel 预设来转换测试环境中的代码。另请参阅 Babel 使用指南

运行

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

您的 package.json 应包含以下配置(其中 <current-version> 是软件包的实际最新版本号)。请添加 scripts 脚本和 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'}],
],
};

现在您已准备就绪!

快照测试

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

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:函数组件与类组件请注意:对于类组件,我们建议使用 Jest 测试 props 而非直接测试方法。

现在使用 React 的测试渲染器和 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();
});

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

__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 命令覆盖现有快照。

此示例代码可在 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 的元素类型检查机制触发,模拟模块无法通过检查。您有以下选择:

  1. 渲染为文本。虽然快照中不会显示传递给模拟组件的 props,但实现简单:

    jest.mock('./SomeComponent', () => () => 'SomeComponent');
  2. 渲染为自定义元素。DOM "自定义元素"不会触发任何检查警告。注意:元素名需小写且包含连字符:

    jest.mock('./Widget', () => () => <mock-widget />);
  3. 使用 react-test-renderer。该渲染器不检查元素类型,例如会乐意接受 SomeComponent 这样的组件。你可以使用测试渲染器检查快照,并配合 Enzyme 分别检查组件行为。

  4. 全局禁用警告(需在 jest 配置文件中设置):

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

    通常不建议选择此方案,因为可能丢失有效警告。但在某些场景下适用,例如测试 react-native 组件时,我们将 react-native 标签渲染到 DOM 中会产生大量无关警告。另一种方案是劫持 console.warn 并抑制特定警告。

DOM 测试

如需对渲染组件进行断言和操作,可使用 @testing-library/reactEnzyme 或 React 的 TestUtils。以下示例使用 @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/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 找到。

自定义转换器

如需更高级的功能,您也可以构建自己的转换器。下面是一个使用 @babel/core 代替 babel-jest 的示例:

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 包。

要在 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'],
});

更多细节请参阅专用文档