组件测试实际应用

若要演示实际测试代码,组件测试是一个很好的切入点。 组件测试比简单的单元测试更有实质性,不如 并演示与 DOM 的交互。更多 从理论上讲,React 的使用让 Web 开发者更容易思考 由各种组件构成的网站或 Web 应用。

因此,测试各个组件,无论它们有多复杂,都是不错的方法 是开始考虑测试新应用或现有应用的一种方式。

本页将详细介绍如何测试包含复杂的外部参数的小型组件 依赖项测试不与任何元素交互的组件非常简单 其他代码,例如点击按钮并确认数字会增加。 实际上,很少有这样的代码, 互动的价值可能有限。

(本文并非一个完整的教程,后面的部分是“自动化测试”部分, 将会逐步介绍如何使用您在实践中学习的示例代码来测试实际网站 可用作教程不过,这个页面仍会介绍一些 实际组件测试。)

被测组件

我们将使用 Vitest 及其 JSDOM 环境来测试 React 组件。这样, 我们可以在模拟浏览器时在命令行中使用 Node 快速运行测试。

<ph type="x-smartling-placeholder">
</ph> 带有
    选择每个名称旁边的“选择”按钮。
一个小型 React 组件,显示一系列 来自广告联盟的用户。

这个名为 UserList 的 React 组件会从网络中提取用户列表 您可以从中选择其中一项使用 fetch 中的 useEffect 内,且选择处理程序通过 Context。其代码如下:

import React, { useEffect, useState, useContext } from 'react';
import { UserContext } from './UserContext.tsx';
import { UserRow } from './UserRow.tsx';

export function UserList({ count = 4 }: { count?: number }) {
  const [users, setUsers] = useState<any[]>([]);
  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/users?_limit=' + count)
      .then((response) => response.json())
      .then((json) => setUsers(json));
  }, [count]);

  const c = useContext(UserContext);
  return (
    <div>
      <h2>Users</h2>
      <ul>
        {users.map((u) => (
          <li key={u.id}>
            <button onClick={() => c.userChosen(u.id)}>Choose</button>{' '}
            <UserRow u={u} />
          </li>
        ))}
      </ul>
    </div>
  );
}

此示例并未演示 React 最佳实践(例如,它使用 useEffect 内的 fetch),但您的代码库可能包含许多 case 非常喜欢更重要的是,这些情况在一开始可能显得难以测试 一览无余。本课程的后续部分将讨论如何编写 。

以下是我们在本示例中要测试的内容:

  • 检查是否创建了一些正确的 DOM 来响应来自网络的数据。
  • 确认点击用户会触发回调。

每个组成部分都与众不同。是什么原因使得测试此测试很有趣?

  • 它使用全局 fetch 从网络中请求真实数据, 在测试中可能会出现不稳定或运行缓慢的情况
  • 它导入了另一个类 UserRow,我们可能不希望对其进行隐式测试。
  • 它使用的 Context 并非具体被测代码的一部分,并且 通常由父组件提供。

先编写一个快速测试

我们可以快速测试有关此组件的一些非常基本的内容。需要明确的是 不是很有用!但在对等环境中设置样板代码 名为 UserList.test.tsx 的文件(请注意,像 Vitest 这样的测试运行程序将 默认情况下,运行以 .test.js 或类似内容结尾的文件,包括 .tsx

import { vi, test, assert, afterAll } from 'vitest';
import { render } from '@testing-library/react';
import { UserList } from './UserList.tsx';
import React, { ContextType } from 'react';

test('render', async () => {
  const c = render(<UserList />);

  const headingNode = await c.findAllByText(/Users);
  assert.isNotNull(headingNode);
});

此测试断言,组件渲染时包含文本“Users”。 即使组件有一个将 fetch 发送到 网络。fetch 在测试结束时仍在进行中,没有 设置端点。我们无法确认 至少不会等待超时。

模拟 fetch()

模拟是指将真实的函数或类替换为 以进行测试这在几乎所有类型的测试中都是很常见的做法, 但最简单的单元测试除外。我们会在断言和 其他基元

您可以为测试模拟 fetch(),使其快速完成并返回 您需要的数据,而不是“真实世界”的数据或未知数据。fetch 是一个全局变量, 也就是说,我们不必将其 importrequire 添加到代码中。

在 vitest 中,您可以使用特殊标记来调用 vi.stubGlobal 来模拟全局变量, 对象 - 这将构建一个模拟,我们稍后可以对其进行修改。vi.fn()这些 方法将在本课程后面的部分详细介绍, 您可以在以下代码中看到它们的实际运用:

test('render', async () => {
  const fetchMock = vi.fn();
  fetchMock.mockReturnValue(
    Promise.resolve({
      json: () => Promise.resolve([{ name: 'Sam', id: 'sam' }]),
    }),
  );
  vi.stubGlobal('fetch', fetchMock);

  const c = render(<UserList />);

  const headingNode = await c.queryByText(/Users);
  assert.isNotNull(headingNode);

  await waitFor(async () => {
    const samNode = await c.queryByText(/Sam);
    assert.isNotNull(samNode);
  });
});

afterAll(() => {
  vi.unstubAllGlobals();
});

此代码添加了一个模拟,用于描述网络提取的版本 Response,然后等待其显示。如果这些文字没有显示 - 如需对此进行检查,您可以将 queryByText 中的查询更改为新名称: 测试将失败

此示例使用了 Vitest 的内置模拟帮助程序,但其他测试 框架采用类似的模拟方法。Vitest 的独特之处在于,您必须 在所有测试后调用 vi.unstubAllGlobals(),或者设置一个等效的全局变量 选项。不执行“撤消”操作我们的工作 fetch 模拟可能会影响其他测试,每个请求都会得到响应 。

模拟导入

您可能已经注意到,UserList 组件本身会导入一个组件 名为 UserRow。虽然我们尚未添加其代码 呈现用户名:之前的测试会检查“Sam”, 直接在 UserList 中呈现,因此它必须来自 UserRow

<ph type="x-smartling-placeholder">
</ph> 创建流程的流程图
  用户的每个名称都在组件中移动
UserListTest没有公开范围 共 UserRow

不过,UserRow 本身可能是一个复杂的组件,可能会进一步提取 用户数据,或具有与我们的测试无关的副作用。删除那个 这将使测试更有帮助,特别是当您要测试的组件 测试会变得越来越复杂,与它们的依赖项更加相互交织。

幸运的是,即使您的测试 不直接使用它们,因此使用它们的任何代码都会提供一个 简单版本或已知版本:

vi.mock('./UserRow.tsx', () => {
  return {
    UserRow(arg) {
      return <>{arg.u.name}</>;
    },
  }
});

test('render', async () => {
  // ...
});

与模拟 fetch 全局变量一样,这也是一个强大的工具,但 如果您的代码具有大量依赖项,则更新是不可持续的。同样, 即编写可测试的代码。

点击并提供背景信息

React 和其他库(如 Lit) 有一个名为 Context 的概念。示例代码包含一个 UserContext, 如果选择了用户,则调用 方法。这通常被视为 “属性钻取”,其中回调将直接传递给 UserList

我们编写的自动化测试框架未提供 UserContext。添加点击 对 React 测试执行操作,否则会导致测试崩溃,或者 最好是在别处提供默认实例, 我们的控件(类似于上面的未知 UserRow):

  const c = render(<UserList />);
  const chooseButton = await c.getByText(/Choose);
  chooseButton.click();

不过,在渲染该组件时,您可以提供自己的 Context。这个 示例使用了 vi.fn()(一个 Vitest 模拟函数)实例, 检查是否进行了调用,以及进行了哪些参数 在本例中,它与上述示例中的模拟 fetch 交互。 并且测试可以确认传递的 ID 是否为“sam”:

  const userChosenFn = vi.fn();
  const ucForTest: ContextType<typeof UserContext> = { userChosen: userChosenFn as any };
  const c = render(
    <UserContext.Provider value={ucForTest}>
      <UserList />
    </UserContext.Provider>,
  );

  const chooseButton = await c.getByText(/Choose);
  chooseButton.click();
  assert.deepStrictEqual(userChosenFn.mock.calls, [['sam']]);

这是一种简单却强大的模式,可方便您移除不相关的 依赖项的依赖项。

总结

这是一个简单明了的示例,演示如何构建 组件测试来测试和保护难以测试的 React 组件; 我们着重于确保组件能与其 依赖项(全局 fetch、导入的子组件和 Context)。

检查您的理解情况

使用哪些方法测试 React 组件?

使用简单的依赖项模拟复杂的依赖项以进行测试
使用 Context 进行依赖项注入
对全局数据执行桩
检查数字是否递增