现代 React 测试(二):Jest 和 Enzyme
如果您喜欢这篇文章,请订阅我的时事通讯。
Enzyme 可能是最流行的 React 组件测试工具。虽然现在它面临着激烈的竞争(参见下一篇文章!),但它仍然被许多团队使用。
这是系列文章的第二篇,我们将学习如何使用 Jest 和 Enzyme 测试 React 组件以及如何应用我们在第一篇文章中学到的最佳实践。
- 现代 React 测试:最佳实践
- 现代 React 测试:Jest 和 Enzyme(本文)
- 现代 React 测试:Jest 和 React 测试库
订阅以了解第三篇文章。
Jest 和 Enzyme 入门
我们将设置并使用这些工具:
为什么使用 Jest 和 Enzyme
与其他测试运行器相比, Jest有许多优势:
- 非常快。
- 交互式监视模式仅运行与您的更改相关的测试。
- 有用的失败消息。
- 配置简单,甚至零配置。
- 嘲讽和间谍。
- 覆盖报告。
- 丰富的匹配器 API。
Enzyme提供类似 jQuery 的 API 来查找元素、触发事件处理程序等等。它曾经是测试 React 组件的权威工具,至今仍非常流行。在这里,我并非试图说服您使用 Enzyme,而只是分享我的使用经验。在本系列的下一篇文章中,我们将探讨一个流行的替代方案——React 测试库。
酶的一些缺点是:
- API 面太大,您需要知道哪些方法是好的,哪些不是。
- 太容易访问组件内部。
- API 并未针对现代测试最佳实践进行优化。
设置 Jest 和 Enzyme
首先,安装所有依赖项,包括对等依赖项:
npm install --save-dev jest react-test-renderer enzyme enzyme-adapter-react-16 node-fetch
您还需要babel-jest(用于 Babel)和ts-jest(用于 TypeScript)。如果您使用 webpack,请确保为环境启用 ECMAScript 模块转换test
。
创建一个src/setupTests.js
文件来定制 Jest 环境:
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
// Configure Enzyme with React 16 adapter
Enzyme.configure({ adapter: new Adapter() });
// If you're using the fetch API
import fetch from 'node-fetch';
global.fetch = fetch;
然后像这样更新package.json
:
{
"name": "pizza",
"version": "1.0.0",
"dependencies": {
"react": "16.8.3",
"react-dom": "16.8.3"
},
"devDependencies": {
"enzyme": "3.9.0",
"enzyme-adapter-react-16": "1.11.2",
"jest": "24.6.0",
"node-fetch": "2.6.0",
"react-test-renderer": "16.8.6"
},
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
},
"jest": {
"setupFilesAfterEnv": ["<rootDir>/src/setupTests.js"]
}
}
该setupFilesAfterEnv
选项告诉 Jest 我们在上一步中创建的安装文件。
创建我们的第一个测试
测试的最佳位置是靠近源代码。例如,如果你有一个位于 的组件src/components/Button.js
,那么针对该组件的测试可以位于src/components/__tests__/Button.spec.js
。Jest 会自动找到并运行该测试。
那么,让我们创建第一个测试:
import React from 'react';
import { mount } from 'enzyme';
test('hello world', () => {
const wrapper = mount(<p>Hello Jest!</p>);
expect(wrapper.text()).toMatch('Hello Jest!');
});
这里我们使用 Enzyme 的mount()方法渲染一段文本,然后使用 Enzyme 的text()
方法和 Jest 的assert测试渲染的树是否包含“Hello Jest!”文本toMatch()
。
运行测试
运行npm test
(或npm t
)运行所有测试。你会看到类似这样的内容:
运行npm run test:watch
以在监视模式下运行 Jest:Jest 将仅运行与自上次提交以来更改的文件相关的测试,并且每次您更改代码时,Jest 都会重新运行这些测试。这是我通常运行 Jest 的方式。即使在大型项目中,运行所有测试需要几分钟,监视模式也足够快。
运行npm run test:coverage
以运行所有测试并生成覆盖率报告。您可以在coverage
文件夹中找到它。
mount() 与 shallow() 与 render()
Enzyme 有三种渲染方法:
-
mount()
渲染整个 DOM 树,并提供类似 jQuery 的 API 来访问树中的 DOM 元素、模拟事件以及读取文本内容。我大多数时候更喜欢这种方法。 -
render()
返回包含渲染 HTML 代码的字符串,类似于renderToString()
from 的方法react-dom
。当你需要测试 HTML 输出时,它很有用。例如,渲染 Markdown 的组件。 -
shallow()
只渲染组件本身,不渲染其子组件。我从不使用这种渲染方式。想象一下,你想点击功能中的一个按钮,然后看到某个地方的文本发生了变化,但很可能按钮和文本都位于子组件内部,所以你最终会测试诸如 props 或 state 之类的内部变量,而这应该避免。更多详情,请参阅 Kent C. Dodds 的文章《为什么我从不使用浅渲染》 。
快照测试
Jest 快照的工作方式如下:您告诉 Jest 您想要确保该组件的输出永远不会意外更改,并且 Jest 将您的组件输出(称为快照)保存到文件中:
exports[`test should render a label 1`] = `
<label
className="isBlock">
Hello Jest!
</label>
`;
每次您或您团队中的某个人更改标记时,Jest 都会显示差异并要求更新快照(如果更改是有意为之)。
您可以使用快照来存储任何值:React 树、字符串、数字、对象等。
快照测试听起来是个好主意,但是存在几个问题:
- 容易提交有 bug 的快照;
- 失败是难以理解的;
- 一个小小的改变就可能导致数百个快照失败;
- 我们倾向于不假思索地更新快照;
- 与低级模块耦合;
- 测试意图难以理解;
- 它们给人一种虚假的安全感。
避免快照测试,除非您正在测试具有明确意图的非常简短的输出(例如类名或错误消息),或者您确实想要验证输出是否相同。
如果您使用快照,请保持快照简短并且优先toMatchInlineSnapshot()
于toMatchSnapshot()
。
例如,不是对整个组件输出进行快照:
test('shows out of cheese error message', () => {
const wrapper = mount(<Pizza />);
expect(wrapper.debug()).toMatchSnapshot();
});
仅拍摄您正在测试的部分:
test('shows out of cheese error message', () => {
const wrapper = mount(<Pizza />);
const error = wrapper.find('[data-testid="errorMessage"]').text();
expect(error).toMatchInlineSnapshot(`Error: Out of cheese!`);
});
选择用于测试的 DOM 元素
一般来说,你的测试应该类似于用户与应用的交互方式。这意味着你应该避免依赖实现细节,因为它们可能会发生变化,你需要更新你的测试。
让我们比较一下选择 DOM 元素的不同方法:
选择器 | 受到推崇的 | 笔记 |
---|---|---|
button ,Button |
绝不 | 最差:太普通 |
.btn.btn-large |
绝不 | 缺点:与风格相关 |
#main |
绝不 | 坏处:一般情况下避免使用 ID |
[data-testid="cookButton"] |
有时 | 好的:对用户不可见,但不是实现细节,在没有更好的选择时使用 |
[alt="Chuck Norris"] ,[role="banner"] |
经常 | 优点:仍然对用户不可见,但已经是应用程序 UI 的一部分 |
[children="Cook pizza!"] |
总是 | 最佳:应用程序 UI 的用户可见部分 |
总结一下:
- 优先选择依赖于用户可见信息(如按钮标签)或辅助技术(如图像
alt
属性或 ARIArole
)的查询。 data-testid
当以上方法均无效时使用。- 避免使用 HTML 元素或 React 组件名称、CSS 类名或 ID 等实现细节。
例如,要在测试中选择此按钮:
<button data-testid="cookButton">Cook pizza!</button>
我们可以通过其文本内容进行查询:
const wrapper = mount(<Pizza />);
wrapper.find({children: "Cook pizza!"]})
或者通过测试ID查询:
const wrapper = mount(<Pizza />);
wrapper.find({'data-testid': "cookButton"]})
两者都是有效的,并且都有其缺点:
- 文本内容可能会发生变化,您需要更新测试。如果您的翻译库在测试中仅渲染字符串 ID,或者您希望测试处理用户在应用中看到的实际文本,那么这可能不是问题。
- 测试 ID 会将仅在测试中需要的 prop 添加到你的标记中,使它们变得杂乱无章。此外,应用用户也看不到测试 ID:即使你从按钮上移除标签,带有测试 ID 的测试仍然会通过。你可能需要进行一些设置,将测试 ID 从发送给用户的标记中移除。
在测试中选择元素时,没有单一的完美方法,但有些方法比其他方法更好。
是否simulate()
在 Enzyme 中有两种方式可以触发事件:
- 使用
simulate()
方法,如wrapper.simulate('click')
; - 直接调用事件处理程序 prop,例如
wrapper.props().onClick()
。
使用哪种方法是酶界的一个大争论。
这个名字simulate()
有点误导:它实际上并没有模拟事件,而是像我们手动调用 prop 一样调用它。这两行代码的作用几乎相同:
wrapper.simulate('click');
wrapper.props().onClick();
在组件中使用 Hooks有一个区别: simulate()会调用测试实用程序中的act()方法,以“使你的测试运行更接近 React 在浏览器中的工作方式”。当你直接在带有 Hooks 的组件上调用事件处理程序时,你会看到来自 React 的警告。
大多数情况下,直接调用事件处理程序(通过调用 prop 或 withsimulate()
方法)与实际浏览器行为之间的差异并不重要,但在某些情况下,这种差异可能会导致您误解测试的行为。例如,如果您simulate()
点击表单中的提交按钮,它不会像真正的提交按钮那样提交表单。
测试 React 组件
查看CodeSandbox 上的所有示例。遗憾的是,CodeSandbox 并不完全支持 Jest,因此某些测试会失败,除非您克隆GitHub 存储库并在本地运行测试。
测试渲染
当您的组件有多个变体并且您想要测试某个 prop 是否呈现正确的变体时,这种测试会很有用。
import React from 'react';
import { mount } from 'enzyme';
import Pizza from '../Pizza';
test('contains all ingredients', () => {
const ingredients = ['bacon', 'tomato', 'mozzarella', 'pineapples'];
const wrapper = mount(<Pizza ingredients={ingredients} />);
ingredients.forEach(ingredient => {
expect(wrapper.text()).toMatch(ingredient);
});
});
在这里,我们正在测试我们的Pizza
组件是否将传递给组件的所有成分作为 prop 进行渲染。
测试用户交互
要“模拟”(参见上面的“是否模拟”)类似或 的simulate()
事件,请直接调用此事件的 prop,然后测试输出:click
change
import React from 'react';
import { mount } from 'enzyme';
import ExpandCollapse from '../ExpandCollapse';
test('button expands and collapses the content', () => {
const children = 'Hello world';
const wrapper = mount(
<ExpandCollapse excerpt="Information about dogs">
{children}
</ExpandCollapse>
);
expect(wrapper.text()).not.toMatch(children);
wrapper.find({ children: 'Expand' }).simulate('click');
expect(wrapper.text()).toMatch(children);
wrapper.update();
wrapper.find({ children: 'Collapse' }).simulate('click');
expect(wrapper.text()).not.toMatch(children);
});
这里我们有一个组件,点击“展开”按钮时会显示一些文本,点击“折叠”按钮时会隐藏文本。我们的测试验证了这一行为。
有关该wrapper.update()
方法的更多信息,请参阅下面的“酶注意事项”部分。
请参阅下一节,了解更复杂的测试事件示例。
测试事件处理程序
当你对单个组件进行单元测试时,事件处理程序通常在父组件中定义,并且响应这些事件时不会出现可见的变化。它们还定义了要测试的组件的 API。
jest.fn()
创建一个模拟函数或间谍函数,使您可以检查该函数被调用的次数以及调用了哪些参数。
import React from 'react';
import { mount } from 'enzyme';
import Login from '../Login';
test('submits username and password', () => {
const username = 'me';
const password = 'please';
const onSubmit = jest.fn();
const wrapper = mount(<Login onSubmit={onSubmit} />);
wrapper
.find({ 'data-testid': 'loginForm-username' })
.simulate('change', { target: { value: username } });
wrapper
.find({ 'data-testid': 'loginForm-password' })
.simulate('change', { target: { value: password } });
wrapper.update();
wrapper.find({ 'data-testid': 'loginForm' }).simulate('submit', {
preventDefault: () => {}
});
expect(onSubmit).toHaveBeenCalledTimes(1);
expect(onSubmit).toHaveBeenCalledWith({
username,
password
});
});
这里我们使用为组件的 propjest.fn()
定义一个间谍,然后使用上一节中描述的技术填写表单,然后我们在元素上调用 prop并检查该函数是否只被调用一次并且是否已收到登录名和密码。onSubmit
Login
onSubmit
<form>
onSubmit
直接触发表单提交处理程序并不理想,因为它可能会导致测试出现误报,但这是我们使用 Enzyme 提交表单的唯一方法。例如,我们无法测试提交按钮是否真的提交了表单。有些人认为这样的测试是在测试浏览器,而不是代码,应该避免。但事实并非如此:有很多方法可以搞乱提交按钮,例如将其放在表单之外或使用type="button"
。
异步测试
异步操作是最难测试的。开发人员常常会放弃,并在测试中添加随机延迟:
const wait = (time = 0) =>
new Promise(resolve => {
setTimeout(resolve, time);
});
test('something async', async () => {
// Run an async operation...
await wait(100).then(() => {
expect(wrapper.text()).toMatch('Done!');
});
});
这种方法存在问题。延迟始终是一个随机数。在开发人员编写代码时,这个数字在开发人员的机器上足够好。但在其他时间、在任何其他机器上,它都可能太长或太短。如果延迟太长,我们的测试将运行超过必要时间。如果延迟太短,我们的测试就会中断。
更好的方法是轮询:等待期望的结果,比如页面上的新文本,通过短间隔多次检查,直到期望结果为真。wait -for-expect库就是这样做的:
import waitForExpect from 'wait-for-expect';
test('something async', async () => {
expect.assertions(1);
// Run an async operation...
await waitForExpect(() => {
expect(wrapper.text()).toMatch('Done!');
});
});
现在我们的测试将等待必要的时间,但不会超过时间。
expect.assertions()
方法对于编写异步测试很有用:你告诉 Jest 你的测试中有多少个断言,如果你搞砸了某些事情,比如忘记返回一个 Promise test()
,那么这个测试就会失败。
请参阅下一节以了解更多现实示例。
测试网络请求和模拟
有很多方法可以测试发送网络请求的组件:
- 依赖注入;
- 模拟服务模块;
- 模拟高级网络 API,例如
fetch
; - 模拟低级网络 API,捕获所有发出网络请求的方式。
我这里不建议向真实的 API 发送真实的网络请求,因为这种方式既慢又脆弱。API 返回的任何网络问题或数据变更都可能破坏我们的测试。此外,你需要为所有测试用例准备正确的数据——这在真实的 API 或数据库中很难实现。
依赖注入是指将依赖项作为函数参数或组件属性传递,而不是将其硬编码在模块内部。这允许您在测试中传递另一个实现。使用默认函数参数或默认组件属性来定义默认实现,该实现应该在非测试代码中使用。这样,您就不必在每次使用函数或组件时都传递依赖项:
import React from 'react';
const defaultFetchIngredients = () => fetch(URL).then(r => r.json());
export default function RemotePizza({ fetchIngredients }) {
const [ingredients, setIngredients] = React.useState([]);
const handleCook = () => {
fetchIngredients().then(response => {
setIngredients(response.args.ingredients);
});
};
return (
<>
<button onClick={handleCook}>Cook</button>
{ingredients.length > 0 && (
<ul>
{ingredients.map(ingredient => (
<li key={ingredient}>{ingredient}</li>
))}
</ul>
)}
</>
);
}
RemotePizza.defaultProps = {
fetchIngredients: defaultFetchIngredients
};
当我们使用组件而不传递fetchIngredients
prop 时,它将使用默认实现:
<RemotePizza />
但在测试中,我们将传递一个自定义实现,它返回模拟数据而不是发出实际的网络请求:
import React from 'react';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import waitForExpect from 'wait-for-expect';
import RemotePizza from '../RemotePizza';
const ingredients = ['bacon', 'tomato', 'mozzarella', 'pineapples'];
test('download ingredients from internets', async () => {
expect.assertions(4);
const fetchIngredients = () =>
Promise.resolve({
args: { ingredients }
});
const wrapper = mount(
<RemotePizza fetchIngredients={fetchIngredients} />
);
await act(async () => {
wrapper.find({ children: 'Cook' }).simulate('click');
});
await waitForExpect(() => {
wrapper.update();
ingredients.forEach(ingredient => {
expect(wrapper.text()).toMatch(ingredient);
});
});
});
请注意,我们在act()
这里的方法中包装了异步操作。
当您渲染直接接受注入的组件时,依赖注入非常适合单元测试,但对于集成测试,需要太多样板来将依赖项传递给深度嵌套的组件。
这就是请求模拟的用武之地。
模拟类似于依赖注入,因为你也在测试中用自己的依赖实现替换依赖实现,但它的工作层次更深:通过修改模块加载或浏览器 API(如)的fetch
工作方式。
你jest.mock()
可以模拟任何 JavaScript 模块。为了使其在我们的例子中正常工作,我们需要将获取函数提取到一个单独的模块中,通常称为服务模块:
export const fetchIngredients = () =>
fetch(
'https://httpbin.org/anything?ingredients=bacon&ingredients=mozzarella&ingredients=pineapples'
).then(r => r.json());
然后将其导入到组件中:
import React from 'react';
import { fetchIngredients } from '../services';
export default function RemotePizza() {
/* Same as above */
}
现在我们可以在测试中模拟它:
import React from 'react';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import waitForExpect from 'wait-for-expect';
import RemotePizza from '../RemotePizza';
import { fetchIngredients } from '../../services';
jest.mock('../../services');
afterEach(() => {
fetchIngredients.mockReset();
});
const ingredients = ['bacon', 'tomato', 'mozzarella', 'pineapples'];
test('download ingredients from internets', async () => {
expect.assertions(4);
fetchIngredients.mockResolvedValue({ args: { ingredients } });
const wrapper = mount(<RemotePizza />);
await act(async () => {
wrapper.find({ children: 'Cook' }).simulate('click');
});
await waitForExpect(() => {
wrapper.update();
ingredients.forEach(ingredient => {
expect(wrapper.text()).toMatch(ingredient);
});
});
});
我们正在使用 Jest 的mockResolvedValue方法来解析具有模拟数据的 Promise。
模拟fetch
API类似于模拟方法,但不是导入方法并用 模拟它jest.mock()
,而是匹配 URL 并给出模拟响应。
我们将使用fetch-mock来模拟 API 请求:
import React from 'react';
import { mount } from 'enzyme';
import fetchMock from 'fetch-mock';
import { act } from 'react-dom/test-utils';
import waitForExpect from 'wait-for-expect';
import RemotePizza from '../RemotePizza';
const ingredients = ['bacon', 'tomato', 'mozzarella', 'pineapples'];
afterAll(() => {
fetchMock.restore();
});
test('download ingredients from internets', async () => {
expect.assertions(4);
fetchMock.restore().mock(/https:\/\/httpbin.org\/anything\?.*/, {
body: { args: { ingredients } }
});
const wrapper = mount(<RemotePizza />);
await act(async () => {
wrapper.find({ children: 'Cook' }).simulate('click');
});
await waitForExpect(() => {
wrapper.update();
ingredients.forEach(ingredient => {
expect(wrapper.text()).toMatch(ingredient);
});
});
});
这里我们使用mock()
fetch-mock 中的方法,对任何与给定 URL 模式匹配的网络请求返回一个模拟响应。其余测试与依赖注入相同。
模拟网络类似于模拟fetch
API,但它在较低级别上工作,因此使用其他 API(如XMLHttpRequest
)发送的网络请求也将被模拟。
我们将使用Nock来模拟网络请求:
import React from 'react';
import { mount } from 'enzyme';
import nock from 'nock';
import { act } from 'react-dom/test-utils';
import waitForExpect from 'wait-for-expect';
import RemotePizza from '../RemotePizza';
const ingredients = ['bacon', 'tomato', 'mozzarella', 'pineapples'];
afterEach(() => {
nock.restore();
});
test('download ingredients from internets', async () => {
expect.assertions(5);
const scope = nock('https://httpbin.org')
.get('/anything')
.query(true)
.reply(200, { args: { ingredients } });
const wrapper = mount(<RemotePizza />);
await act(async () => {
wrapper.find({ children: 'Cook' }).simulate('click');
});
await waitForExpect(() => {
wrapper.update();
expect(scope.isDone()).toBe(true);
ingredients.forEach(ingredient => {
expect(wrapper.text()).toMatch(ingredient);
});
});
});
代码与 fetch-mock 几乎相同,但这里我们定义了一个范围:请求 URL 和模拟响应的映射。
query(true)
表示我们正在匹配具有任何查询参数的请求,否则您可以定义特定的参数,例如query({quantity: 42})
。
scope.isDone()
是true
范围内定义的所有请求都已提出。
我会在jest.mock()
和 Nock 之间进行选择:
jest.mock()
已经可以通过 Jest 使用了,您不需要设置和学习任何新内容 - 它的工作方式与模拟任何其他模块相同。- Nock 的 API 比 fetch-mock 更便捷,并且提供了调试工具。它还可以记录真实的网络请求,因此您无需手动编写模拟响应。
酶注意事项
update()
方法
Enzyme 的update()函数非常神奇。它的文档是这样描述的:
强制重新渲染。如果外部组件可能正在更新某个位置的状态,则在检查渲染输出之前运行此命令很有用。
有人在某处做某事。我找不到任何关于何时需要使用它的逻辑。所以我的经验法则是:编写测试时不要使用它,直到看到过时的渲染输出。然后update()
在 之前添加expect()
。
请注意,您只能调用update()
包装器实例:
const wrapper = mount(<Pizza />);
// Someone doing something somewhere...
wrapper.update();
expect(wrapper.text()).toMatch('wow much updates');
hostNodes()
方法
假设你有一个按钮组件:
const Button = props => <button className="Button" {...props} />;
您有一个表格:
<form>
<Button data-testid="pizzaForm-submit">Cook pizza!</Button>
</form>
并且您尝试在测试中模拟单击此按钮:
wrapper.find('[data-testid="pizzaForm-submit"]').simulate('click');
这不起作用,因为find()
返回两个节点:一个用于Button
React 组件,一个用于button
HTML 元素,因为组件树看起来像这样:
<Button data-testid="pizzaForm-submit">
<button className="Button" data-testid="pizzaForm-submit">Cook pizza!</button>
</Button>
为了避免这种情况,您需要使用 Enzyme 的hostNodes()方法:
wrapper
.find('[data-testid="pizzaForm-submit"]')
.hostNodes()
.simulate('click');
hostNodes()
方法仅返回主机节点:在 React DOM 中主机节点是 HTML 元素。
重复使用find()
查询
find()
在测试中缓存和重用查询时要小心,如下所示:
const input = wrapper.find('[data-testid="quantity"]');
expect(input.prop('value')).toBe('0'); // -> Pass
如果您更改输入的值并尝试重新使用该input
变量来测试它,它将会失败:
input.simulate('change', { target: { value: '42' } });
expect(input.prop('value')).toBe('42'); // -> Fail!
expect(input.prop('value')).toBe('0'); // -> Pass
发生这种情况是因为input
变量仍然保留对初始组件树的引用。
为了解决这个问题,我们需要find()
在更改输入的值后再次运行查询:
const findInput = wrapper => wrapper.find('[data-testid="quantity"]');
expect(findInput(wrapper).prop('value')).toBe('0'); // -> Pass
findInput(wrapper).simulate('change', { target: { value: '42' } });
expect(findInput(wrapper).prop('value')).toBe('42'); // -> Pass
我通常不会在测试中重复使用任何查询,而是编写一些像findInput
上面这样的辅助函数。这为我节省了大量的调试时间。
act()
帮手
使用 React Test Utilities 中的act()方法包装交互的“单元”,例如渲染、用户事件或数据获取,以使您的测试更好地模拟用户与您的应用程序的交互方式。
Enzymeact()
在某些方法中为您调用该方法,例如simulate()
,但在某些情况下您需要在测试中手动使用它。
测试食谱页面对该方法有更好的解释act()
和更多的使用示例。
调试
有时您想检查渲染的 React 树,请使用debug()方法:
const wrapper = mount(<p>Hello Jest!</p>);
console.log('LOL', wrapper.debug());
// -> <p>Hello Jest!</p>
您还可以打印元素:
console.log('LOL', wrapper.find({ children: 'Expand' }).debug());
结论
我们已经学习了如何设置 Enzyme 以及如何测试不同的 React 组件。
在下一篇文章中,我们将研究 React Testing Library 以及它与 Enzyme 的比较。
如果您喜欢这篇文章,请订阅我的时事通讯。
鏂囩珷鏉ユ簮锛�https://dev.to/sapegin/modern-react-testing-part-2-jest-and-enzyme-46kk