现代 React 测试(二):Jest 和 Enzyme

2025-06-10

现代 React 测试(二):Jest 和 Enzyme

如果您喜欢这篇文章,请订阅我的时事通讯。

Enzyme 可能是最流行的 React 组件测试工具。虽然现在它面临着激烈的竞争(参见下一篇文章!),但它仍然被许多团队使用。

这是系列文章的第二篇,我们将学习如何使用 Jest 和 Enzyme 测试 React 组件以及如何应用我们在第一篇文章中学到的最佳实践。

订阅以了解第三篇文章。

Jest 和 Enzyme 入门

我们将设置并使用这些工具:

  • Jest,一个测试运行器;
  • Enzyme,一个用于 React 的测试实用程序;

为什么使用 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
Enter fullscreen mode Exit fullscreen mode

您还需要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;
Enter fullscreen mode Exit fullscreen mode

然后像这样更新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"]
  }
}
Enter fullscreen mode Exit fullscreen mode

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!');
});
Enter fullscreen mode Exit fullscreen mode

这里我们使用 Enzyme 的mount()方法渲染一段文本,然后使用 Enzyme 的text()方法和 Jest 的assert测试渲染的树是否包含“Hello Jest!”文本toMatch() 。

运行测试

运行npm test(或npm t)运行所有测试。你会看到类似这样的内容:

在终端中运行 Jest 和 Enzyme 测试

运行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>
`;
Enter fullscreen mode Exit fullscreen mode

每次您或您团队中的某个人更改标记时,Jest 都会显示差异并要求更新快照(如果更改是有意为之)。

您可以使用快照来存储任何值:React 树、字符串、数字、对象等。

快照测试听起来是个好主意,但是存在几个问题

  • 容易提交有 bug 的快照;
  • 失败是难以理解的;
  • 一个小小的改变就可能导致数百个快照失败;
  • 我们倾向于不假思索地更新快照;
  • 与低级模块耦合;
  • 测试意图难以理解;
  • 它们给人一种虚假的安全感。

避免快照测试,除非您正在测试具有明确意图的非常简短的输出(例如类名或错误消息),或者您确实想要验证输出是否相同。

如果您使用快照,请保持快照简短并且优先toMatchInlineSnapshot()toMatchSnapshot()

例如,不是对整个组件输出进行快照:

test('shows out of cheese error message', () => {
  const wrapper = mount(<Pizza />);
  expect(wrapper.debug()).toMatchSnapshot();
});
Enter fullscreen mode Exit fullscreen mode

仅拍摄您正在测试的部分:

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!`);
});
Enter fullscreen mode Exit fullscreen mode

选择用于测试的 DOM 元素

一般来说,你的测试应该类似于用户与应用的交互方式。这意味着你应该避免依赖实现细节,因为它们可能会发生变化,你需要更新你的测试。

让我们比较一下选择 DOM 元素的不同方法:

选择器 受到推崇的 笔记
buttonButton 绝不 最差:太普通
.btn.btn-large 绝不 缺点:与风格相关
#main 绝不 坏处:一般情况下避免使用 ID
[data-testid="cookButton"] 有时 好的:对用户不可见,但不是实现细节,在没有更好的选择时使用
[alt="Chuck Norris"][role="banner"] 经常 优点:仍然对用户不可见,但已经是应用程序 UI 的一部分
[children="Cook pizza!"]  总是 最佳:应用程序 UI 的用户可见部分

总结一下:

  • 优先选择依赖于用户可见信息(如按钮标签)或辅助技术(如图像alt属性或 ARIA role)的查询。
  • data-testid 当以上方法均无效时使用。
  • 避免使用 HTML 元素或 React 组件名称、CSS 类名或 ID 等实现细节。

例如,要在测试中选择此按钮:

<button data-testid="cookButton">Cook pizza!</button>
Enter fullscreen mode Exit fullscreen mode

我们可以通过其文本内容进行查询:

const wrapper = mount(<Pizza />);
wrapper.find({children: "Cook pizza!"]})
Enter fullscreen mode Exit fullscreen mode

或者通过测试ID查询:

const wrapper = mount(<Pizza />);
wrapper.find({'data-testid': "cookButton"]})
Enter fullscreen mode Exit fullscreen mode

两者都是有效的,并且都有其缺点:

  • 文本内容可能会发生变化,您需要更新测试。如果您的翻译库在测试中仅渲染字符串 ID,或者您希望测试处理用户在应用中看到的实际文本,那么这可能不是问题。
  • 测试 ID 会将仅在测试中需要的 prop 添加到你的标记中,使它们变得杂乱无章。此外,应用用户也看不到测试 ID:即使你从按钮上移除标签,带有测试 ID 的测试仍然会通过。你可能需要进行一些设置,将测试 ID 从发送给用户的标记中移除。

在测试中选择元素时,没有单一的完美方法,但有些方法比其他方法更好。

是否simulate()

在 Enzyme 中有两种方式可以触发事件:

  • 使用simulate()方法,如wrapper.simulate('click')
  • 直接调用事件处理程序 prop,例如wrapper.props().onClick()

使用哪种方法是酶界的一个大争论。

这个名字simulate()有点误导:它实际上并没有模拟事件,而是像我们手动调用 prop 一样调用它。这两行代码的作用几乎相同:

wrapper.simulate('click');
wrapper.props().onClick();
Enter fullscreen mode Exit fullscreen mode

在组件中使用 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);
  });
});
Enter fullscreen mode Exit fullscreen mode

在这里,我们正在测试我们的Pizza组件是否将传递给组件的所有成分作为 prop 进行渲染。

测试用户交互

要“模拟”(参见上面的“是否模拟”)类似或 的simulate()事件,请直接调用此事件的 prop,然后测试输出:clickchange

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);
});
Enter fullscreen mode Exit fullscreen mode

这里我们有一个组件,点击“展开”按钮时会显示一些文本,点击“折叠”按钮时会隐藏文本。我们的测试验证了这一行为。

有关该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
  });
});
Enter fullscreen mode Exit fullscreen mode

这里我们使用为组件的 propjest.fn() 定义一个间谍,然后使用上一节中描述的技术填写表单,然后我们在元素上调用 prop并检查该函数是否只被调用一次并且是否已收到登录名和密码。onSubmitLoginonSubmit<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!');
  });
});
Enter fullscreen mode Exit fullscreen mode

这种方法存在问题。延迟始终是一个随机数。在开发人员编写代码时,这个数字在开发人员的机器上足够好。但在其他时间、在任何其他机器上,它都可能太长或太短。如果延迟太长,我们的测试将运行超过必要时间。如果延迟太短,我们的测试就会中断。

更好的方法是轮询:等待期望的结果,比如页面上的新文本,通过短间隔多次检查,直到期望结果为真。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!');
  });
});
Enter fullscreen mode Exit fullscreen mode

现在我们的测试将等待必要的时间,但不会超过时间。

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
};
Enter fullscreen mode Exit fullscreen mode

当我们使用组件而不传递fetchIngredientsprop 时,它将使用默认实现:

<RemotePizza />
Enter fullscreen mode Exit fullscreen mode

但在测试中,我们将传递一个自定义实现,它返回模拟数据而不是发出实际的网络请求:

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);
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

请注意,我们在act() 这里的方法中包装了异步操作。

当您渲染直接接受注入的组件时,依赖注入非常适合单元测试,但对于集成测试,需要太多样板来将依赖项传递给深度嵌套的组件。

这就是请求模拟的用武之地。

模拟类似于依赖注入,因为你也在测试中用自己的依赖实现替换依赖实现,但它的工作层次更深:通过修改模块加载或浏览器 API(如)的fetch工作方式。

jest.mock()可以模拟任何 JavaScript 模块。为了使其在我们的例子中正常工作,我们需要将获取函数提取到一个单独的模块中,通常称为服务模块

export const fetchIngredients = () =>
  fetch(
    'https://httpbin.org/anything?ingredients=bacon&ingredients=mozzarella&ingredients=pineapples'
  ).then(r => r.json());
Enter fullscreen mode Exit fullscreen mode

然后将其导入到组件中:

import React from 'react';
import { fetchIngredients } from '../services';

export default function RemotePizza() {
  /* Same as above */
}
Enter fullscreen mode Exit fullscreen mode

现在我们可以在测试中模拟它:

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);
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

我们正在使用 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);
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

这里我们使用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);
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

代码与 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');
Enter fullscreen mode Exit fullscreen mode

hostNodes()方法

假设你有一个按钮组件:

const Button = props => <button className="Button" {...props} />;
Enter fullscreen mode Exit fullscreen mode

您有一个表格:

<form>
  <Button data-testid="pizzaForm-submit">Cook pizza!</Button>
</form>
Enter fullscreen mode Exit fullscreen mode

并且您尝试在测试中模拟单击此按钮:

wrapper.find('[data-testid="pizzaForm-submit"]').simulate('click');
Enter fullscreen mode Exit fullscreen mode

这不起作用,因为find() 返回两个节点:一个用于ButtonReact 组件,一个用于buttonHTML 元素,因为组件树看起来像这样:

<Button data-testid="pizzaForm-submit">
  <button className="Button" data-testid="pizzaForm-submit">Cook pizza!</button>
</Button>
Enter fullscreen mode Exit fullscreen mode

为了避免这种情况,您需要使用 Enzyme 的hostNodes()方法:

wrapper
  .find('[data-testid="pizzaForm-submit"]')
  .hostNodes()
  .simulate('click');
Enter fullscreen mode Exit fullscreen mode

hostNodes() 方法仅返回主机节点:在 React DOM 中主机节点是 HTML 元素。

重复使用find() 查询

find() 在测试中缓存和重用查询时要小心,如下所示:

const input = wrapper.find('[data-testid="quantity"]');
expect(input.prop('value')).toBe('0'); // -> Pass
Enter fullscreen mode Exit fullscreen mode

如果您更改输入的值并尝试重新使用该input变量来测试它,它将会失败:

input.simulate('change', { target: { value: '42' } });
expect(input.prop('value')).toBe('42'); // -> Fail!
expect(input.prop('value')).toBe('0'); // -> Pass
Enter fullscreen mode Exit fullscreen mode

发生这种情况是因为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
Enter fullscreen mode Exit fullscreen mode

我通常不会在测试中重复使用任何查询,而是编写一些像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>
Enter fullscreen mode Exit fullscreen mode

您还可以打印元素:

console.log('LOL', wrapper.find({ children: 'Expand' }).debug());
Enter fullscreen mode Exit fullscreen mode

结论

我们已经学习了如何设置 Enzyme 以及如何测试不同的 React 组件。

在下一篇文章中,我们将研究 React Testing Library 以及它与 Enzyme 的比较。

如果您喜欢这篇文章,请订阅我的时事通讯。

鏂囩珷鏉ユ簮锛�https://dev.to/sapegin/modern-react-testing-part-2-jest-and-enzyme-46kk
PREV
我为什么退出开源
NEXT
Node 版本管理器(NVM):如何安装和使用(分步指南)