当前位置: 首页 > article >正文

Jest进阶知识:React组件的单元测试

在现代前端开发中,组件是构建应用程序的基本单元。一个组件不仅拥有完整的功能,还能极大地提高代码的复用性。因此,在进行单元测试时,对重要组件进行测试是必不可少的。

Testing Library

Testing Library 是一个专门用于测试 Web 组件的工具库,其设计理念是“测试组件的行为而不是实现细节”。通过 Testing Library 提供的一系列 API,可以模拟浏览器中的用户交互方式,使测试更加贴近真实使用场景。

Jest 与 Testing Library 的关系

  • Jest:一个完整的测试框架,提供了匹配器、mock 库、断言工具等,旨在提供一个全面的测试工具链。
  • Testing Library:一个测试工具库,专注于测试组件的行为。它可以与各种框架结合使用,提供了一组用于测试 React 组件的工具,如 renderscreenfireEvent 等。

常用的 Testing Library 扩展库

  • @testing-library/react:提供了一组用于测试 React 组件的工具,如 renderscreenfireEvent
  • @testing-library/jest-dom:提供了一组 Jest 断言方法,用于测试 DOM 元素的状态和行为,如 toBeInTheDocumenttoHaveTextContent 等。
  • @testing-library/user-event:提供了一组用于模拟用户行为的工具,如 typeclicktab 等。

核心 API

render 方法

render 方法接收一个组件作为参数,将其渲染为 DOM 元素,并返回一个包含重要属性的对象:

  • container:渲染后的 DOM 元素,可用于模拟用户行为或进行断言验证。
  • baseElement:整个文档的根元素 <html>
  • asFragment:将渲染后的 DOM 元素转换为 DocumentFragment 对象,便于进行快照测试。
  • debug:在控制台输出渲染后的 DOM 元素的 HTML 结构,便于调试。

screen 对象

screen 对象封装了一系列常用的 DOM 查询和操作函数:

  • getByLabelText:根据 <label> 元素的 for 属性或内部文本,获取与之关联的表单元素。
  • getByText:根据文本内容获取元素。
  • getByRole:根据 role 属性获取元素。
  • getByPlaceholderText:根据 placeholder 属性获取表单元素。
  • getByTestId:根据 data-testid 属性获取元素。
  • queryBy*:类似于 getBy*,但当元素不存在时返回 null 而不是抛出异常。

测试组件示例

示例一:隐藏消息组件

import { useState } from "react";

function HiddenMessage({ children }) {
    const [isShow, setIsShow] = useState(false);
    return (
        <div>
            <label htmlFor="toggle">显示信息</label>
            <input
                type="checkbox"
                name="toggle"
                id="toggle"
                checked={isShow}
                onChange={(e) => setIsShow(e.target.checked)}
            />
            {isShow ? children : null}
        </div>
    );
}

export default HiddenMessage;

该组件接收一个子组件,并根据复选框的状态决定是否显示子组件。以下是对应的测试代码:

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

test("能够被勾选,功能正常", () => {
    const testMessage = "这是一条测试信息";
    render(<HiddenMessage>{testMessage}</HiddenMessage>);
    // 初始状态下,信息不应显示
    expect(screen.queryByText(testMessage)).toBeNull();
    // 模拟点击复选框
    fireEvent.click(screen.getByLabelText("显示信息"));
    // 信息应显示
    expect(screen.getByText(testMessage)).toBeInTheDocument();
});

示例二:登录组件

import * as React from "react";

function Login() {
    const [state, setState] = React.useReducer(
        (s, a) => ({ ...s, ...a }),
        { resolved: false, loading: false, error: null }
    );

    function handleSubmit(event) {
        event.preventDefault();
        const { usernameInput, passwordInput } = event.target.elements;

        setState({ loading: true, resolved: false, error: null });

        window
            .fetch("/api/login", {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({
                    username: usernameInput.value,
                    password: passwordInput.value,
                }),
            })
            .then((r) =>
                r.json().then((data) => (r.ok ? data : Promise.reject(data)))
            )
            .then(
                (user) => {
                    setState({ loading: false, resolved: true, error: null });
                    window.localStorage.setItem("token", user.token);
                },
                (error) => {
                    setState({ loading: false, resolved: false, error: error.message });
                }
            );
    }

    return (
        <div>
            <form onSubmit={handleSubmit}>
                <div>
                    <label htmlFor="usernameInput">Username</label>
                    <input id="usernameInput" />
                </div>
                <div>
                    <label htmlFor="passwordInput">Password</label>
                    <input id="passwordInput" type="password" />
                </div>
                <button type="submit">
                    Submit{state.loading ? "..." : null}
                </button>
            </form>
            {state.error ? <div role="alert">{state.error}</div> : null}
            {state.resolved ? (
                <div role="alert">Congrats! You're signed in!</div>
            ) : null}
        </div>
    );
}

export default Login;

该组件处理用户的登录请求,根据请求结果显示不同的信息。以下是对应的测试代码:

import { rest } from "msw";
import { setupServer } from "msw/node";
import { render, screen, fireEvent } from "@testing-library/react";
import Login from "../Login";

const fakeUserRes = { token: "fake_user_token" };
const server = setupServer(
    rest.post("/api/login", (req, res, ctx) => {
        return res(ctx.json(fakeUserRes));
    })
);

// 启动服务器
beforeAll(() => server.listen());
// 关闭服务器
afterAll(() => server.close());
// 每个测试用例完成后重置服务器状态
afterEach(() => {
    server.resetHandlers();
    window.localStorage.removeItem("token");
});

test("测试请求成功", async () => {
    render(<Login />);
    fireEvent.change(screen.getByLabelText(/Username/i), {
        target: { value: "xiejie" },
    });
    fireEvent.change(screen.getByLabelText(/Password/i), {
        target: { value: "123456" },
    });
    fireEvent.click(screen.getByText("Submit"));

    expect(await screen.findByRole("alert")).toHaveTextContent(/Congrats/i);
    expect(window.localStorage.getItem("token")).toEqual(fakeUserRes.token);
});

test("测试请求失败", async () => {
    server.use(
        rest.post("/api/login", (req, res, ctx) => {
            return res(ctx.status(500), ctx.json({ message: "服务器内部出错" }));
        })
    );

    render(<Login />);
    fireEvent.change(screen.getByLabelText(/Username/i), {
        target: { value: "xiejie" },
    });
    fireEvent.change(screen.getByLabelText(/Password/i), {
        target: { value: "123456" },
    });
    fireEvent.click(screen.getByText("Submit"));

    expect(await screen.findByRole("alert")).toHaveTextContent(/服务器内部出错/i);
    expect(window.localStorage.getItem("token")).toBeNull();
});

结论

通过本文的介绍,我们了解了如何使用 Testing Library 和 Jest 对 React 组件进行单元测试。通过对组件的行为进行测试,可以确保组件在不同情况下的表现符合预期,从而提高代码的可靠性和可维护性。


http://www.kler.cn/a/386250.html

相关文章:

  • Exploring Defeasible Reasoning in Large Language Models: A Chain-of-Thought A
  • layui的table组件中,对某一列的文字设置颜色为浅蓝怎么设置
  • AutoHotKey自动热键AHK-正则表达式
  • Linux—进程学习-02
  • redis bind 127.0.0.1和bind 10.34.56.78的区别
  • 2024-11-13 学习人工智能的Day26 sklearn(2)
  • 万字长文详解JavaScript基础语法--前端--前端样式--JavaWeb
  • redis 写入权限配置
  • 常用的 Lambda 表达式案例解析
  • 《 C++ 修炼全景指南:十九 》想懂数据库?深入 B 树的世界,揭示高效存储背后的逻辑
  • 不加锁解决线程安全
  • AWS账号安全:如何防范与应对账号被盗风险
  • 【mysql相关】
  • 使用ChatGPT神速精读文献,12个高阶ChatGPT提示词指令,值得你复制使用
  • 哪些人群适合考取 PostgreSQL 数据库 PGCM 证书?
  • 【C++练习】使用海伦公式计算三角形面积
  • CDN到底是什么?
  • 《IDE 使用技巧与插件推荐》
  • 从xss到任意文件读取
  • vue组件传参的八种方式详细总结
  • qt QFile详解
  • 拓扑排序(C++类封装+数组模拟队列和邻接表)
  • 代码随想录之双指针刷题总结
  • wordpress判断page页与非page页
  • 【图论】图的C++实现代码
  • Python小白学习教程从入门到入坑------第二十八课 文件基础操作文件读写(语法进阶)