测试 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
- Yarn
- pnpm
npm install --save-dev react-test-renderer
yarn add --dev react-test-renderer
pnpm add --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
- Yarn
- pnpm
npm install --save-dev jest babel-jest @babel/preset-env @babel/preset-react react-test-renderer
yarn add --dev jest babel-jest @babel/preset-env @babel/preset-react react-test-renderer
pnpm add --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"
}
}
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:
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:
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 test
或 jest
时,将生成如下输出文件:
¥When you run yarn test
or jest
, this will produce an output file like this:
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:
-
渲染为文本。这样你就不会在快照中看到传递给模拟组件的 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');
-
渲染为自定义元素。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 />);
-
使用
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. -
一起禁用警告(应该在你的 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/react、Enzyme 或 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
- Yarn
- pnpm
npm install --save-dev @testing-library/react
yarn add --dev @testing-library/react
pnpm add --save-dev @testing-library/react
让我们实现一个在两个标签之间交换的复选框:
¥Let's implement a checkbox which swaps between two labels:
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>
);
}
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
:
'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/core
和 babel-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.