为了账号安全,请及时绑定邮箱和手机立即绑定

React 测试库(RTL)—— 第一部分

React测试库(RTL)和Jest的快速入门

首先,你需要知道Testing Library 包含多个库,帮助你模拟用户操作来测试UI组件。

照片由Ferenc AlmasiUnsplash拍摄。

Testing Library 并不是专门为 React 设计的,但在 React 社区中很受欢迎并且越来越受到关注。

作为一般规则,最好别用这个测试库来测试,

  1. 组件内部的状态
  2. 组件内部的方法
  3. 组件生命周期中的方法
  4. 子组件

我们为什么要用 Jest 和 Testing Library 呢?

由于Testing Library不是一个测试框架,所以你需要用到Jest(或者其他框架比如Mocha)来收集并适当地运行测试文件。

换句话说,Jest 会确保,当你运行 npm run test 时,所有的测试文件都会被运行,并且结果会显示在终端里。

测试概述

如果你使用的是 React v16 或更高版本,首先安装 React 的测试库:

# 以下为安装命令,保持原样即可。
npm install --save @testing-library/react
npm install --save-dev @testing-library/react @testing-library/dom # 安装 React 和 DOM 的测试库

假设一个初始的React应用如这样的所示

    // src/App.js

    function App() {  
      return (  
        <div className="App">  
          <header className="App-header">  
            <img src={logo} className="App-logo" alt="logo" />  
            <p>  
              编辑 <code>src/App.js</code> 并保存来重新加载。  
            </p>  
            <a  
              className="App-link"  
              href="https://reactjs.org"  
              target="_blank"  
              rel="noopener noreferrer"  
            >  
              学习 React 了解更多  
            </a>  
          </header>  
        </div>  
      );  
    }

有两种开始写测试的方法:

  1. 将测试文件 App.test.js 放在它的源文件(如 App.js)附近。
  2. 创建一个 src/__tests__ 文件夹,并将所有测试文件放进去。

无论如何,这样做比较好:将测试文件命名为源文件,并在其后添加 .test.[扩展名]。扩展名可以为 js, jsx, tstsx

我会按照第一个约定行事。

    // 从'@testing-library/react'导入render和screen
    import { render, screen } from '@testing-library/react';
    import App from './App';

    test('显示学习React的链接', () => {
      render(<App />);
      const linkElement = screen.getByText(/学习React/i);
      expect(linkElement).toBeInTheDocument();
    });

下面的代码是用于测试React应用的一个案例,结构如下:

  1. test 函数定义了一个名为 “renders learn react link” 的测试用例。
  2. render(<App />); 这一行渲染了 App 组件以进行测试。它基本上创建了一个 DOM 表示的副本。
  3. const linkElement = screen.getByText(/learn react/i); 这一行使用 getByText 来查找匹配正则表达式 /learn react/i 的元素。我们知道目前页面上只有一个这样的元素。
  4. 最后,通过使用 expect(linkElement).toBeInTheDocument(); 来测试该元素是否在文档中。语法中的最后部分是一个匹配器,用于检查某个内容,在这种情况下是 .toBeInTheDocument()。你可以在这里找到更多关于 Jest 匹配器RTL 匹配器 的信息,了解更多详情。

这是一个使用RTL的基础测试。

瑞典测试一个简单的表单

用 React Testing Library 测试一个简单的表单

让我们在 src/components/UserForm.js 创建一个简单的用户表单,然后在 App.js 里引入它。

    // src/components/UserForm.js  

    import React, { useState } from "react";  

    const UserForm = ({ onUserAdd }) => {  
      const [name, setName] = useState("");  
      const [email, setEmail] = useState("");  

      const handleSubmit = (e) => {  
        e.preventDefault();  
        onUserAdd({ name, email });  
        setName("");  
        setEmail("");  
      };  

      return (  
        <form onSubmit={handleSubmit}>  
          <div>  
            <label>  
              姓名:  
              <input  
                type="text"  
                value={name}  
                onChange={(e) => setName(e.target.value)}  
                required  
                placeholder="请输入姓名"  
              />  
            </label>  
          </div>  
          <div>  
            <label>  
              邮箱:  
              <input  
                type="email"  
                value={email}  
                onChange={(e) => setEmail(e.target.value)}  
                required  
                placeholder="请输入邮箱"  
              />  
            </label>  
          </div>  
          <button type="submit">提交</button>  
        </form>  
      );  
    };  

    export default UserForm;

正如前面所说,我们会在源代码文件附近创建一个名为 UserForm.test.js 的测试文件,比如 src/components/UserForm.test.js

我们想要确保有两个带有正确标签的字段,并且点击提交会触发 onUserAdd 函数。

试一下表单里的字段

我们从导入必要的库和工具以及需要测试的组件开始。

    // UserForm.test.js  

    import React from "react";  
    import { render, screen } from "@testing-library/react";  
    import UserForm from "./UserForm";

最简单来说,我们可以写一个测试来验证表单上有两个带有正确标签的字段,如下。

    // UserForm.test.js

    import React from "react";
    import { render, screen } from "@testing-library/react";
    import UserForm from "./UserForm";

    const mockOnUserAdd = jest.fn(); // 模拟用户添加函数

    test("进行测试渲染两个带有正确标签的输入框,且每个输入框都有相应的类型", () => {
      render(<UserForm onUserAdd={mockOnUserAdd} />);

      const nameInput = screen.getByLabelText("Name:"); // 获取名称输入框
      expect(nameInput).toBeVisible(); // 验证名称输入框可见
      expect(nameInput).toHaveAttribute("type", "text"); // 验证名称输入框具有属性类型为"text"

      const emailInput = screen.getByLabelText(/Email:/i); // 获取电子邮件输入框,进行不区分大小写的搜索
      expect(emailInput).toBeVisible(); // 验证电子邮件输入框可见
      expect(emailInput).toHaveAttribute("type", "email"); // 验证电子邮件输入框具有属性类型为"email"
    });

因为我们将 onUserAdd 作为属性传递,我们需要模拟实现该函数以便能够单独测试组件。只需这样做:const mockOnUserAdd = jest.fn();

在测试中,我们使用 render 函数将 React 组件渲染到虚拟 DOM 中,这是测试前必不可少的一步。在测试环境中安排组件,并对其操作和断言结果也需要这一步。

由于我们将函数传递给组件,因此我们需要传递模拟函数来模仿真实函数的行为,并断言它是否被调用,被调用的次数和参数等(稍后再说)。

我们还使用 React Testing Library 提供的 screen 工具,查询新组件在用户操作时的表现(在虚拟 DOM 内)。

通过使用代码 screen.getByLabelText('Name:'),RTL 会寻找带有用户可见标签文本 'Name:' 的元素。

我们希望输入位于文档中,并且也期望该元素具有正确的类型,即 textemail。分别是文本类型 text 和电子邮件类型 email

最好你应该经常跑测试,使用TDD或者使用npm run test -a来监控测试,这样每次相关文件有改动时,测试套件都会自动运行。

测试点击

我们来试试点击提交按钮吧。

由于我们已经在之前的测试中定义了模拟的函数,我们可以像之前那样渲染UserForm组件。

我们依然获取名字和电子邮件输入,就像之前一样。我们使用 screen.getByRole('button', { name: 'Submit' }); 来获取带有“Submit”文本的按钮元素。

    // UserForm.test.js  

    ...  

    test("在点击提交按钮时调用 onUserAdd", () => {  
      render(<UserForm onUserAdd={mockOnUserAdd} />);  

      const nameInput = screen.getByLabelText(/Name:/i);  
      const emailInput = screen.getByLabelText(/Email:/i);  
      const submitButton = screen.getByRole("button", { name: "Submit" });  

      fireEvent.change(nameInput, { target: { value: "John Doe" } });  
      fireEvent.change(emailInput, { target: { value: "john.doe@example.com" } });  

      fireEvent.click(submitButton);  

      expect(mockOnUserAdd).toHaveBeenCalledTimes(1); // 期望 mockOnUserAdd 被调用一次  
    });

我们使用 React Testing Library 的 fireEvent 来模拟浏览器中可能会发生的实际事件,像点击按钮这样的操作。记得在引入时加上 fireEvent

然后,fireEvent.change 模拟 DOM 的变化。在上述示例中,我们选择一个输入元素并更改其值。

最后,我们用 fireEvent.click(submitButton); 来模拟点击按钮。

最后一行,expect(mockOnUserAdd).toHaveBeenCalledTimes(1); 断言模拟的函数被调用了一次。这可能行得通,但最好让它异步,因为点击事件不是严格的同步事件。

换句话说,事件API是同步的,因为事件触发是同步的。但是,与之相反,比如点击事件等DOM事件是异步的,因为它们由外部触发,例如用户点击。

因此,我们应该这样添加 async/await(这并不是很好,我们之后会进一步讨论一下)。

    // UserForm.test.js

    ...

    test("点击提交按钮时,调用onUserAdd", async () => {
      // 准备
      render(<UserForm onUserAdd={mockOnUserAdd} />);

      const nameInput = screen.getByLabelText(/Name:/i);
      const emailInput = screen.getByLabelText(/Email:/i);
      const submitButton = screen.getByRole("button");

      // 操作
      await fireEvent.change(nameInput, { target: { value: 'John Doe' } }); // 更改姓名输入框的值为 'John Doe'
      await fireEvent.change(emailInput, { target: { value: 'john.doe@example.com' } }); // 更改电子邮件输入框的值为 'john.doe@example.com'
      await fireEvent.click(submitButton);

      // 验证
      expect(mockOnUserAdd).toHaveBeenCalledTimes(1);
    });

请注意,我们尽量按照 Arrange、Act、Assert 方法。

  • Arrange : 准备好要测试的组件
  • Act : 处理 DOM 中的元素
  • Assert : 验证预期
React 测试的最佳实践

根据上面的测试结果,有几点好的做法值得大家注意。

所以你应该确保每次测试都能独立运行并重新开始,确保它们互不影响。应该在每次测试前后清除所有模拟,如下。

    // 这是一个用户表单的测试文件
    // UserForm.test.js

    import React from "react"; // 导入React库
    import { render, screen } from "@testing-library/react"; // 导入测试库的render和screen函数
    import UserForm from "./UserForm";

    beforeEach(() => {
      mockOnUserAdd.mockClear(); // 清除mockOnUserAdd的模拟
    });

    afterEach(() => {
      jest.clearAllMocks(); // 清除所有的模拟
    });

    ...

有些操作可能会重复,可能最好保持DRY(DRY代表不要重复自己,Don't Repeat Yourself)。

我们可以在 afterEach 之下定义一个函数,例如:const getName = () => screen.getByLabelText(/Name:/i);,然后在需要获取元素时调用它:const nameInput = getName();

这里有一些小建议,更多关于RTL(从右到左)的内容将会陆续更新。

在我的下一篇文章中,我将把 fireEvent 更改为 userEvent。通常推荐使用 userEvent,因为它有助于编写更贴近用户实际操作的测试,这使得测试更接近实际用户行为,因此更加强大和可靠。

相关链接:Jest matchersRTL matchers.

React 测试库 (RTL) — 第 2 部分: 快速入门和 userEventlevelup.gitconnected.com
React Testing Library (RTL) 实践技巧 — 第 3 部分 (Part 3) 使用 React Testing Library (RTL) Playground 可以帮助你更容易地理解你正在编写的测试。
点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消