likes
comments
collection
share

🐙单元测试🐙初探---React Testing Libary

作者站长头像
站长
· 阅读数 47

🐙单元测试🐙初探---React Testing Libary

前段时间我们项目组的架构给我们的项目添加了单元测试的功能,也要求我们开始写一些单元测试代码。这部分也是第一次接触,只能说项目组前端人多的好处就是项目里面的东西比较多,不像以前的项目,一个基于Vue2的项目做了一年多,就是做一些基本的业务功能,里面也没啥新东西。

我们项目是React的项目,单元测试也是基于@testing-library/react进行的。本篇文章主要做一些比较基础的入门;因为实战还很少,甚至可以说没啥实战,等后面实战多了,积累多了,再进行总结和记忆吧。

话不多说,直接开整。

什么是单元测试

单元测试就像是一场考试,测试你的成绩,回想起以往的周考,月考等等,真是往事不堪回首啊。

那既然是考试,那你写的单元测试就像是试卷,而考试的对象就是你的代码,而我们这些程序员就像是老师,那些测试的代码就像是我们的学生一样。

我们可以通过单元测试,来检验这些学生的水平,看有没有出错,有没有遗漏的地方。如果学生答对了,则测试通过,反之,则测试不通过。

当然我们在出题的过程中,也要给出对应的答案,例如:

期望(学生的回答)是(标准答案) --> expect(xxx).toBe(xxx)

如果学生的回答和标准答案一致,则可以判定测试通过,反之则判定失败。

而这个出题、验证和进行判断的过程,我们可以叫做断言

当然上述的讲解,只是简单的描述,单元测试肯定更加复杂的多。这里我们简单理解即可,等你用多了,可以更加深入了解。当然也是我现在理解的比较通俗吧。

React Testing Library 介绍

React Testing Library是一个用于测试React组件的工具库,它提供了一组API,用于模拟用户与React组件的交互,并对组件的渲染结果进行断言和验证。与其他测试工具相比,React Testing Library更注重于测试的用户行为和交互,而不是组件的内部实现细节。

🐙单元测试🐙初探---React Testing Libary

React Testing Library与各种测试工具(如Jest)和断言库(如React Test Renderer)兼容,并且与React生态系统紧密集成,因此在React项目中广泛应用,并且还提供了良好的文档和示例。

React Testing Library 的设计理念是从用户的角度出发,关注组件的行为和交互,而不是关注组件的内部实现细节。这样能够更好地测试组件的行为是否符合用户预期,并且帮助开发者编写更稳定、可靠的代码。

测试流程

使用React Testing Library进行测试通常遵循以下步骤:

  1. 安装React Testing Library:首先,在项目中安装React Testing Library。你可以使用npm或者yarn进行安装。

    npm install --save-dev @testing-library/react
    

    或者

    yarn add --dev @testing-library/react
    
  2. 编写测试用例:创建一个测试文件,通常以.test.js.spec.js结尾。在测试文件中,编写测试用例来测试React组件的行为和状态。

  3. 导入必要的库和组件:在测试文件中,导入React Testing Library提供的测试工具和你要测试的React组件。

    import { render, screen } from '@testing-library/react';
    import MyComponent from './MyComponent';
    
  4. 编写测试用例:编写测试用例来测试组件的各种行为和状态。使用React Testing Library提供的API来选择组件的DOM元素、触发事件、断言组件的行为和状态等。

    test('renders component correctly', () => {
      render(<MyComponent />);
      // Use queries to select DOM elements
      const buttonElement = screen.getByRole('button', { name: 'Click me' });
      // Assert component behavior or state
      expect(buttonElement).toBeInTheDocument();
    });
    
  5. 运行测试:运行测试用例,确保组件按预期工作。通常使用测试运行器(如Jest)来运行测试。

    npm test
    

    或者

    yarn test
    
  6. 重复步骤4和5:根据需要编写更多的测试用例来覆盖组件的不同行为和状态,确保组件的功能和可靠性。

通过这些步骤,你可以使用React Testing Library编写和运行测试,以确保React组件按预期工作并且具有正确的行为和状态。

测试用例编写步骤

创建一个与被测试组件相对应的测试文件。通常,这个文件会放在 __tests__ 文件夹内或直接与组件文件放在同一目录下。文件名通常以 .test.js.spec.js 结尾。

例如,如果你有一个 Button.js 组件,测试文件可以是 Button.test.js

  1. 导入必要模块

    在测试文件中导入需要的库和组件。通常包括 React、被测试的组件、React Testing Library 的方法,以及 Jest 的断言库。

    import React from 'react';
    import { render, screen, fireEvent } from '@testing-library/react';
    import '@testing-library/jest-dom/extend-expect'; // 为了额外的匹配器方法,如 toBeInTheDocument
    import Button from './Button'; // 假设 Button 组件和测试文件在同一目录
    
  2. 编写测试用例

    在 React Testing Library 中,screen 是一个全局对象,它简化了对渲染结果进行查询的过程。使用 screen 可以更方便地访问和断言渲染后的 DOM 元素。screen 提供了一系列查询方法,这些方法通常是由 getByqueryByfindBy 等前缀组成。

    使用 testit 方法编写测试用例。这些方法接受两个参数:测试的描述(字符串)和执行测试的函数。

    test('renders the button and handles click events', () => {
      // 渲染组件
      render(<Button />);
    
      // 查找按钮元素
      const buttonElement = screen.getByText('Click me');
    
      // 断言按钮存在于文档中
      expect(buttonElement).toBeInTheDocument();
    
      // 模拟点击事件
      fireEvent.click(buttonElement);
    
      // 查找消息元素
      const messageElement = screen.getByText('Button clicked!');
    
      // 断言消息存在于文档中
      expect(messageElement).toBeInTheDocument();
    });
    
  3. 渲染组件

    使用 render 方法渲染你的 React 组件。render 方法会返回包含对渲染结果进行查询的工具。

    const { getByText } = render(<Button />);
    
  4. 查询元素

    利用 React Testing Library 提供的查询方法如 screen.getByText, screen.getByRole 等查找 DOM 元素。

    const buttonElement = screen.getByText('Click me');
    
  5. 触发事件

    使用 fireEventuserEvent 模拟用户交互。fireEvent 是 React Testing Library 内置的方法,而 userEvent 更接近真实用户行为,可以单独安装和使用。

    fireEvent.click(buttonElement);
    
  6. 断言结果

    使用 Jest 提供的 expect 方法进行断言,验证组件的行为是否符合预期。

    expect(buttonElement).toBeInTheDocument();
    const messageElement = screen.getByText('Button clicked!');
    expect(messageElement).toBeInTheDocument();
    

示例

以下是完整的测试文件示例:

// Button.test.js
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import Button from './Button';

test('renders the button and handles click events', () => {
  // 渲染组件
  render(<Button />);

  // 查找按钮元素
  const buttonElement = screen.getByText('Click me');

  // 断言按钮存在于文档中
  expect(buttonElement).toBeInTheDocument();

  // 模拟点击事件
  fireEvent.click(buttonElement);

  // 查找消息元素
  const messageElement = screen.getByText('Button clicked!');

  // 断言消息存在于文档中
  expect(messageElement).toBeInTheDocument();
});

React Testing Library 常用API

React Testing Library提供了一系列常用的API,用于测试React组件。

这里我们按照上一章节的测试用例的编写步骤来讲述一下��用的API。

组件渲染

在React Testing Library中,用于组件渲染的常用API是render()函数。这个函数用于将React组件渲染到虚拟DOM中,并返回一个对象,该对象包含了与该组件相关的查询方法。

下面是render()函数的基本用法和示例:

import { render } from '@testing-library/react';
import MyComponent from './MyComponent';

test('renders component correctly', () => {
  // 渲染组件
  const { getByText } = render(<MyComponent />);
  
  // 通过文本内容获取DOM元素
  const textElement = getByText('Hello, World!');
  
  // 断言组件是否渲染正确
  expect(textElement).toBeInTheDocument();
});

在这个示例中,render()函数用于将<MyComponent />组件渲染到虚拟DOM中。然后,使用getByText()方法通过文本内容获取DOM元素。最后,使用toBeInTheDocument()断言方法检查获取到的DOM元素是否存在于DOM中。

查询元素

React Testing Library 提供了一系列强大的查询API,用于查找和验证DOM中的元素。这些查询方法分为几类,每一类都有其特定的用途和特点。

我们看一下官网总结的查询元素的表格:

Type of Query0 Matches1 Match>1 MatchesRetry (Async/Await)
Single Element
getBy...Throw errorReturn elementThrow errorNo
queryBy...Return nullReturn elementThrow errorNo
findBy...Throw errorReturn elementThrow errorYes
Multiple Elements
getAllBy...Throw errorReturn arrayReturn arrayNo
queryAllBy...Return []Return arrayReturn arrayNo
findAllBy...Throw errorReturn arrayReturn arrayYes

这里我们可以看到:

查询元素有三种方法,分别是:get,queryfind。通过这些方法我们可以查询对应的单个元素,如果想查找全部元素的话,只需要加All字段即可,类似于querySelectquerySelectorAll

那这三种方法有什么差异呢,为什么会有这三个,难道一个不能直接通用吗?

这里我们可以根据上面的表格,来对这三个方法进行讲述:

get 方法

  • 同步查询get 方法是同步的,立即返回匹配的元素。
  • 找不到元素时抛出错误:如果没有找到匹配的元素,会抛出一个错误。
  • 常用场景:适用于你确信元素应该立即存在于 DOM 中的情况。

query 方法

  • 同步查询query 方法也是同步的,立即返回匹配的元素。
  • 找不到元素时返回 null:如果没有找到匹配的元素,不会抛出错误,而是返回 null
  • 常用场景:适用于测试某个元素不应该存在的情况。

find 方法

  • 异步查询find 方法是异步的,返回一个 Promise。
  • 等待元素出现或超时:在等待匹配的元素出现时,默认会等待一定的时间(通常是 1000ms),如果没有找到元素,会抛出一个错误。
  • 常用场景:适用于异步渲染的元素,比如组件加载数据后显示的内容。

示例:

import { render, screen } from '@testing-library/react';
import MyComponent from './MyComponent';

test('renders component with async text', async () => {
  render(<MyComponent />);
  const element = await screen.findByText('Async Text');
  expect(element).toBeInTheDocument();
});

总结

  • get 方法:同步,找不到元素时抛出错误,适用于元素应立即存在的情况。
  • query 方法:同步,找不到元素时返回 null,适用于测试元素不应该存在的情况。
  • find 方法:异步,返回 Promise,适用于元素异步出现的情况。

其中get 方法是最常用到的,除非测试元素不应该存在的情况,会使用query 方法,和测试元素异步出现的情况,使用find 方法,其他情况一般都用get 方法。`

All的方法,直接看表格即可,这里就不进行再次赘述了。

以下是一些常用的查询元素的 API:

getBy...方法在传递字符串的情况下,查找规则是精准匹配,可以正则的形式进行模糊匹配。其他方法也是相同的。

getByRole:查找具有特定角色的元素。通常用于查找按钮、链接等具有 ARIA 角色的元素。

const button = getByRole('button', { name: /submit/i });

getByText:查找包含特定文本内容的元素。常用于查找包含特定文字的任意元素。

const linkElement = getByText(/learn react/i);

getByLabelText:查找关联特定文本标签的表单控件。用于查找带有 <label> 标签的表单输入元素。

const input = getByLabelText(/username/i);

getByPlaceholderText:查找具有特定占位符文本的表单控件。用于查找带有 placeholder 属性的输入元素。

const input = getByPlaceholderText(/enter your username/i);

getByAltText:查找具有特定 alt 属性文本的图像元素。用于查找带有 alt 属性的 <img> 元素。

const image = getByAltText(/product image/i);

getByTitle:查找具有特定 title 属性的元素。用于查找带有 title 属性的任意元素。

const element = getByTitle(/close/i);

getByTestId:查找具有特定 data-testid 属性的元素。用于查找带有 data-testid 属性的元素,通常用于测试中特定元素的选择。

const element = getByTestId('custom-element');

这些 getBy 查询函数都是同步的,并且在找不到匹配的元素时会抛出错误,因此适合用于确保元素存在的情况。使用这些函数,可以更高效地编写可靠的测试用例。

这里只介绍常见的getBy 查询函数,其他的queryBy...findBy...和与之对应的查找全部的方法,和上面的getBy 查询函数类似,只是前面的查询方法不同而已。

这里再强调一下:

其中get 方法是最常用到的,除非测试元素不应该存在的情况,会使用query 方法,和测试元素异步出现的情况,使用find 方法,其他情况一般都用get 方法。`

触发事件

在 React Testing Library 中,触发事件通常使用 fireEventuserEvent 两个工具。fireEvent 是 React Testing Library 内置的,而 userEvent 则是一个更高级的用户模拟工具,提供了更接近用户实际行为的事件触发方法。

fireEvent

fireEvent 是 React Testing Library 内部的一部分,是一个用于触发 DOM 事件的工具,适用于大多数事件类型。你可以使用 fireEvent 来模拟各种用户操作,例如点击、输入、聚焦等。

常用的 fireEvent API

  • click:模拟点击事件。
  • change:模拟输入框的值变化事件。
  • submit:模拟表单提交事件。
  • focus:模拟聚焦事件。
  • blur:模拟失焦事件。
  • keydownkeyupkeypress:模拟键盘事件。

示例:

import { render, screen, fireEvent } from '@testing-library/react';
import MyComponent from './MyComponent';

test('handles button click', () => {
  render(<MyComponent />);
  const button = screen.getByRole('button', { name: 'Click me' });
  
  // 模拟点击事件
  fireEvent.click(button);
  
  // 断言点击后的结果
  expect(screen.getByText('Button clicked')).toBeInTheDocument();
});

userEvent

userEvent@testing-library/user-event 库提供的,用于模拟用户操作,提供了更高级和更符合用户交互习惯的事件触发方法。

首先,确保安装了 @testing-library/user-event 包:

pnpm install --save-dev @testing-library/user-event

常用的 userEvent API

  • click:模拟点击事件。
  • type:模拟用户输入事件。
  • clear:清除输入框的内容。
  • selectOptions:选择下拉菜单的选项。
  • deselectOptions:取消选择下拉菜单的选项。
  • upload:模拟文件上传事件。
  • tab:模拟 tab 键切换焦点事件。
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import MyComponent from './MyComponent';

test('handles input change', async () => {
  render(<MyComponent />);
  const input = screen.getByPlaceholderText('Enter text');
  
  // 模拟输入事件
  await userEvent.type(input, 'Hello, World!');
  
  // 断言输入后的结果
  expect(input).toHaveValue('Hello, World!');
});

选择 fireEventuserEvent

  • fireEvent: 更基础,更直接地触发事件,更适合简单的事件。
  • userEvent: 更高层次,更接近真实用户行为,适合模拟复杂的用户交互。

建议在大多数情况下使用 userEvent,因为它能够更准确地模拟用户行为,从而使测试更加可靠和易读。但在一些简单的场景下,fireEvent 也足够使用。

断言结果

在使用 React Testing Library 进行测试时,断言(assertion)是验证组件行为和状态的关键步骤。React Testing Library 通常与 Jest 一起使用,Jest 提供了丰富的断言方法,而 React Testing Library 通过其扩展库 @testing-library/jest-dom 提供了一些额外的 DOM 相关的断言方法。

Jest 内置的断言方法

Jest 本身提供了一系列通用的断言方法,用于各种类型的验证。

常用的 Jest 断言方法

  • expect(value).toBe(expected): 验证两个值是否相等(严格相等)。
  • expect(value).toEqual(expected): 验证两个对象或数组的结构是否相等。
  • expect(value).toBeNull(): 验证值是否为 null
  • expect(value).toBeUndefined(): 验证值是否为 undefined
  • expect(value).toBeTruthy(): 验证值是否为真(truthy)。
  • expect(value).toBeFalsy(): 验证值是否为假(falsy)。

@testing-library/jest-dom 提供的断言方法

@testing-library/jest-dom 扩展了 Jest 的断言方法,提供了一些专门用于 DOM 元素的断言工具,这些工具更直观,更适合测试 UI 组件。

安装 @testing-library/jest-dom

首先需要安装 @testing-library/jest-dom

npm install @testing-library/jest-dom

在测试文件中引入 @testing-library/jest-dom

import '@testing-library/jest-dom/extend-expect';

常用的 @testing-library/jest-dom 断言方法

  • toBeInTheDocument(): 验证元素是否存在于文档中。

    expect(element).toBeInTheDocument();
    
  • toBeVisible(): 验证元素是否可见。

    expect(element).toBeVisible();
    
  • toHaveTextContent(text): 断言元素具有特定的文本内容。

    expect(element).toHaveTextContent('Hello, World!');
    
  • toHaveAttribute(name, value): 验证元素是否具有指定属性及其值。

    expect(element).toHaveAttribute('type', 'submit');
    
  • toBeDisabled():断言元素被禁用。

    expect(element).toBeDisabled();
    
  • toBeChecked():断言复选框或单选按钮被选中。

    expect(checkbox).toBeChecked();
    
  • toHaveFocus():断言元素获得焦点。

    expect(input).toHaveFocus();
    
  • toHaveValue():断言表单元素具有特定的值。

    expect(input).toHaveValue('Initial value');
    
  • toHaveClass(className): 验证元素是否具有指定的 CSS 类。

    expect(element).toHaveClass('my-class');
    
  • toHaveStyle(css): 验证元素是否具有指定的内联样式。

    expect(element).toHaveStyle('display: none');
    
  • toHaveFormValues():断言表单元素具有特定的值。

    expect(form).toHaveFormValues({
      username: 'johndoe',
      password: 'password123',
    });
    
  • toContainElement():断言一个元素包含另一个元素。

    expect(container).toContainElement(childElement);
    
  • toBeEmptyDOMElement():断言元素没有任何子节点。

    expect(element).toBeEmptyDOMElement();
    

以下是一些示例代码,展示了如何使用这些断言方法:

import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect'; // 引入 jest-dom 扩展
import MyComponent from './MyComponent';

test('renders the component with correct text', () => {
    render(<MyComponent />);
    const linkElement = screen.getByText(/hello world/i);
    expect(linkElement).toBeInTheDocument();
    expect(linkElement).toBeVisible();
    expect(linkElement).toHaveTextContent('Hello World');
});

test('input field has correct placeholder and value', () => {
    render(<MyComponent />);
    const inputElement = screen.getByPlaceholderText(/enter your name/i);
    expect(inputElement).toBeInTheDocument();
    expect(inputElement).toHaveAttribute('type', 'text');
    expect(inputElement).toHaveValue('');
});

test('button has correct class and is disabled', () => {
    render(<MyComponent />);
    const buttonElement = screen.getByRole('button', { name: /submit/i });
    expect(buttonElement).toBeInTheDocument();
    expect(buttonElement).toHaveClass('submit-button');
    expect(buttonElement).toBeDisabled();
});

通过结合 Jest 和 @testing-library/jest-dom 提供的断言方法,你可以编写清晰且有力的测试用例,验证组件的各种行为和状态。这些断言方法使得你的测试更加语义化和易读,从而提高了测试的可靠性和维护性。

常见的全局函数和钩子函数

  1. beforeAll(fn, timeout):

    • 在所有测试用例开始前执行,通常用于设置测试环境。
    • fn: 函数,要执行的操作。
    • timeout: 可选参数,设置超时时间(毫秒)。
    beforeAll(() => {
      // 执行一些初始化操作
    });
    
  2. afterAll(fn, timeout):

    • 在所有测试用例结束后执行,通常用于清理测试环境。
    • fn: 函数,要执行的清理操作。
    • timeout: 可选参数,设置超时时间(毫秒)。
    afterAll(() => {
      // 执行一些清理操作
    });
    
  3. beforeEach(fn, timeout):

    • 在每个测试用例开始前执行,通常用于重置测试状态。
    • fn: 函数,要执行的操作。
    • timeout: 可选参数,设置超时时间(毫秒)。
    beforeEach(() => {
      // 执行一些每个测试用例前的操作
    });
    
  4. afterEach(fn, timeout):

    • 在每个测试用例结束后执行,通常用于清理测试状态。
    • fn: 函数,要执行的清理操作。
    • timeout: 可选参数,设置超时时间(毫秒)。
    afterEach(() => {
      // 执行一些每个测试用例后的操作
    });
    
  5. describe(name, fn):

    • 定义一个测试套件(组),用来组织一组相关的测试用例。
    • name: 字符串,套件(组)的名称。
    • fn: 函数,包含测试用例的回调函数。
    describe('Math operations', () => {
      test('adds 1 + 2 to equal 3', () => {
        expect(1 + 2).toBe(3);
      });
    });
    
  6. test(name, fn, timeout):

    • 定义一个测试用例。
    • name: 字符串,测试用例的名称。
    • fn: 函数,包含测试用例的回调函数。
    • timeout: 可选参数,设置超时时间(毫秒)。
    test('adds 1 + 2 to equal 3', () => {
      expect(1 + 2).toBe(3);
    });
    

模拟后台接口

在使用 React Testing Library 测试依赖于后台接口数据的组件时,我们通常会模拟这些接口调用。这可以通过以下几种方式实现:

  1. Mocking Fetch API:使用 Jest 来模拟全局 fetch 函数。
  2. Mocking Axios:如果使用 Axios 作为 HTTP 客户端,可以使用 Jest 来模拟 Axios。
  3. Mock Service Worker (MSW):一种更为强大和灵活的方式,模拟浏览器的网络请求。

下面我将分别介绍这几种方法。

Mocking Fetch API

假设我们有一个组件 UserComponent,它在挂载时从 API 获取用户数据:

import React, { useEffect, useState } from 'react';

function UserComponent() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch('/api/user')
      .then(response => response.json())
      .then(data => setUser(data));
  }, []);

  if (!user) {
    return <div>Loading...</div>;
  }

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

export default UserComponent;

测试示例:

import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import UserComponent from './UserComponent';

beforeEach(() => {
  global.fetch = jest.fn(() =>
    Promise.resolve({
      json: () => Promise.resolve({ name: 'John Doe', email: 'john.doe@example.com' }),
    })
  );
});

afterEach(() => {
  global.fetch.mockClear();
});

test('fetches and displays user data', async () => {
  render(<UserComponent />);

  await waitFor(() => screen.getByText('John Doe'));

  expect(screen.getByText('John Doe')).toBeInTheDocument();
  expect(screen.getByText('john.doe@example.com')).toBeInTheDocument();
});

Mocking Axios

如果你使用 Axios 作为 HTTP 客户端,可以使用 Jest 来模拟 Axios 请求。

组件示例:

import React, { useEffect, useState } from 'react';
import axios from 'axios';

function UserComponent() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    axios.get('/api/user').then(response => {
      setUser(response.data);
    });
  }, []);

  if (!user) {
    return <div>Loading...</div>;
  }

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

export default UserComponent;

测试示例:

import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import axios from 'axios';
import UserComponent from './UserComponent';

jest.mock('axios');

test('fetches and displays user data', async () => {
  axios.get.mockResolvedValue({ data: { name: 'John Doe', email: 'john.doe@example.com' } });

  render(<UserComponent />);

  await waitFor(() => screen.getByText('John Doe'));

  expect(screen.getByText('John Doe')).toBeInTheDocument();
  expect(screen.getByText('john.doe@example.com')).toBeInTheDocument();
});

Mock Service Worker (MSW)

Mock Service Worker (MSW) 是一个用于拦截和模拟浏览器网络请求的工具。它提供了更灵活和真实的模拟 HTTP 请求的方式。

安装 MSW:

npm install msw --save-dev

设置 MSW:

  1. 创建 mock server 配置文件(如 src/mocks/browser.js):
import { setupWorker, rest } from 'msw';

export const worker = setupWorker(
  rest.get('/api/user', (req, res, ctx) => {
    return res(ctx.json({ name: 'John Doe', email: 'john.doe@example.com' }));
  })
);
  1. 在测试文件中启动 mock server
import { render, screen, waitFor } from '@testing-library/react';
import { setupServer } from 'msw/node';
import { rest } from 'msw';
import UserComponent from './UserComponent';

// 设置服务端
const server = setupServer(
  rest.get('/api/user', (req, res, ctx) => {
    return res(ctx.json({ name: 'John Doe', email: 'john.doe@example.com' }));
  })
);

// 启动服务端
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

test('fetches and displays user data', async () => {
  render(<UserComponent />);

  await waitFor(() => screen.getByText('John Doe'));

  expect(screen.getByText('John Doe')).toBeInTheDocument();
  expect(screen.getByText('john.doe@example.com')).toBeInTheDocument();
});

总结

  • Mocking Fetch API:适合直接使用 fetch 的情况,通过全局 fetch 函数进行模拟。
  • Mocking Axios:适合使用 Axios 作为 HTTP 客户端的情况,通过 Jest 模拟 Axios 请求。
  • Mock Service Worker (MSW):更为灵活和强大的解决方案,适合复杂的网络请求模拟场景。

选择合适的方法来模拟后台接口数据,可以帮助你更好地测试依赖于 API 调用的组件。

其他

act

act 的作用是将你对组件状态或 DOM 的更新包裹起来,确保这些更新在测试中得到正确处理。这在测试异步操作或需要等待某些更新时特别有用。

异步示例

对于包含异步操作的组件,我们通常需要在 act 中处理这些异步操作。

假设我们有一个组件,点击按钮后异步获取数据并更新显示:

import React, { useState } from 'react';

function AsyncComponent() {
  const [data, setData] = useState(null);

  const fetchData = () => {
    setTimeout(() => {
      setData('Hello World');
    }, 1000);
  };

  return (
    <div>
      <button onClick={fetchData}>Fetch Data</button>
      {data ? <span>{data}</span> : <span>Loading...</span>}
    </div>
  );
}

我们可以这样测试它:

import { render, screen, fireEvent, act } from '@testing-library/react';
import AsyncComponent from './AsyncComponent';

test('fetches and displays data', async () => {
  render(<AsyncComponent />);
  
  const button = screen.getByText('Fetch Data');
  
  act(() => {
    fireEvent.click(button);
  });

  // 等待异步操作完成
  await act(async () => {
    await new Promise(resolve => setTimeout(resolve, 1000));
  });

  expect(screen.getByText('Hello World')).toBeInTheDocument();
});
  • act 是确保在测试中所有更新都被正确处理的关键工具。
  • 在同步操作中,act 将状态更新或事件处理包裹起来。
  • 在异步操作中,act 需要与 async/await 一起使用,确���异步操作完成并处理所有副作用。

使用 act 可以确保你的测试更加稳定和可靠,避免由于未处理的状态更新或副作用导致的测试失败。

renderHook

renderHook 用于测试自定义 Hook。它使得我们能够单独测试 Hook 的逻辑,而不必将其嵌入到组件中。

用法:

import { renderHook } from '@testing-library/react-hooks';

const { result } = renderHook(() => useCustomHook());

示例:

假设我们有一个自定义 Hook useCounter

import { useState } from 'react';

function useCounter() {
  const [count, setCount] = useState(0);
  const increment = () => setCount(count + 1);
  return { count, increment };
}

我们可以这样测试它:

import { renderHook, act } from '@testing-library/react-hooks';
import useCounter from './useCounter';

test('should increment counter', () => {
  const { result } = renderHook(() => useCounter());

  act(() => {
    result.current.increment();
  });

  expect(result.current.count).toBe(1);
});

renderHook:用于渲染和测试自定义 Hook,简化 Hook 的独立测试。

waitFor

waitFor 是一个异步的工具函数,允许我们等待某个条件变为真。在测试异步代码或需要等待某些 DOM 更新时非常有用。

用法:

import { waitFor } from '@testing-library/react';

await waitFor(() => {
  // 期待某个条件为真
  expect(someElement).toBeInTheDocument();
});

示例:

假设我们有一个组件,在异步获取数据后更新 DOM:

import React, { useEffect, useState } from 'react';

function FetchComponent() {
  const [data, setData] = useState(null);

  useEffect(() => {
    setTimeout(() => {
      setData('Hello World');
    }, 1000);
  }, []);

  if (!data) {
    return <div>Loading...</div>;
  }

  return <div>{data}</div>;
}

我们可以这样测试它:

import { render, screen, waitFor } from '@testing-library/react';
import FetchComponent from './FetchComponent';

test('renders fetched data', async () => {
  render(<FetchComponent />);

  // 验证 'Loading...' 文本出现
  expect(screen.getByText('Loading...')).toBeInTheDocument();

  // 等待并验证 'Hello World' 文本出现
  await waitFor(() => {
    expect(screen.getByText('Hello World')).toBeInTheDocument();
  });
});

waitFor:用于等待异步操作完成,通常用于等待 DOM 更新或状态变化。

单元测试有什么好处呢

使用React Testing Library进行单元测试有以下好处:

  1. 增强代码的稳定性和可靠性:通过编写单元测试,可以及早捕获潜在的bug和错误,从而提高代码的质量和稳定性。单元测试可以帮助开发者在修改或重构代码时快速发现问题,并减少引入新问题的风险。
  2. 起到很好的说明文档作用:React Testing Library鼓励编写清晰、简洁和易于理解的测试代码。它专注于用户界面的行为和交互,而不是关注组件的内部实现细节。这样可以使测试代码更易读、易于维护,并且可以更好地与其他开发者共享和理解。
  3. 提升团队合作和沟通:React Testing Library提供了一种标准化的测试方法,使得团队成员之间的沟通更加容易。开发者可以更容易地理解测试代码,共享测试用例,并进行讨论和反馈。这有助于提高团队的合作效率和代码质量。
  4. 促进组件的正确交互:React Testing Library鼓励测试从用户的角度出发,通过模拟用户与组件的交互来检查组件的行为是否正确。这种方法可以更好地测试组件的交互逻辑,确保用户在实际使用时获得预期的体验。
  5. 支持重构和代码重用:在编写测试时,需要对组件的可测试性进行考虑。通过将测试与组件的内部实现解耦,可以更轻松地进行重构和代码重用。这有助于保持代码的灵活性和可维护性,并减少对测试的影响。

总的来说,使用React Testing Library进行单元测试可以提供可读性强的测试代码、促进组件的正确交互、增强代码的稳定性和可靠性、支持重构和代码重用,以及提升团队合作和沟通。这些好处有助于改善开发流程,提高代码质量,并提供更好的用户体验。

参考文献

转载自:https://juejin.cn/post/7381373703081738280
评论
请登录