使用 Jest + Testing Library 完成 React 的单元测试

盖锐
2023-12-01

说明

不同于普通JS测试,组件的测试会更关注 DOM 渲染,以及组件功能正确性,而不是组件内部某些方法调用等的测试

Testing Library 包含 DOM 及 UI 组件测试的一系列工具, 支持 React Vue Angular 等多个框架

组件测试目标

  • 编写可维护的测试,并通过测试可以确保组件在正确工作
  • 同时希望测试避免包含实现细节,以便组件重构时,不会破坏测试用例或者减慢开发速度
  • 针对 React 组件来说,应避免测试 state、声明周期、组件方法等
  • Testing Library 测试的时渲染后 dom 而不是未渲染前 React Dom

相关

Testing Library 简单使用

测试 React 组件需要使用的技术

  • @testing-library/dom: 一个轻量级的(DOM 查询、交互)测试解决方案,它使用了一种与 ”用户的在页面中查找元素" 类似的DOM 查询方式,以保证准确性
  • @testing-library/user-event: 提供了更加高级的浏览器交互模拟 – 即事件
  • @testing-library/react: 在 @testing-library/dom 基础上,将 React 组件渲染为 DOM 便于后边测试
  • @testing-library/jest-dom 追加一系列辅助测试 DOM 的 matchers 匹配器,需要在每个 react test 文件的顶部引用,否则类似 expect(dom).toBeInTheDocument() 这样的断言则没法用

由于 react 组件使用jsx 编写,所以要在基础的 babel 配置中加上对 react 的支持,否则 jest 会不认识 react 组件代码

// babel.config.js
module.exports = {
  presets: [
    ['@babel/preset-env', {targets: {node: 'current'}}],
    '@babel/preset-react', // 追加
    '@babel/preset-typescript',
  ],
};

查询语句说明:查询语句分为三大类:getBy…\queryBy…\findBy…

  • getBy… 查找匹配的节点,如果没找到或者找到了多个,则会抛出错误
  • queryBy… 同样是查找匹配的节点,没找到的话不会抛出错误,通常用于断言不存在的元素(找到多个还是会抛出错误)
  • findBy… 也是查找匹配的节点,但是可以支持查找异步元素,会返回 promise
  • 如果预计的匹配项会有多个则可以使用以上三个API的变种 getAllBy…\queryAllBy…\findAllBy…
  • 更多细节查看

查询项说明

  • getByRole(tagName, {name: '通过 name 过滤'}) 这里的 role 指的是浏览器 ARIA 无障碍机制中的属性(可用的 role),name 指的则是 aria-label 属性的值
  • getByText(string) 通过文本内容来查找元素,但是尽量不要用在 div span p 等元素的查找上
  • getByAltText(string) 通过 alt 属性查找 img area input 等元素
  • getByPlaceholderText(string) 通过 placeholder 查询元素
  • getByTestId(string) 通过给元素设置 data-testid 然后可以借助这个查询获得这个元素
  • container.querySelector('') 借助 render 后返回的 container 使用 DOM 的方式查询元素
  • 查询语句的第一个参数可以写作正则表达式,用来模糊匹配
  • 更多查询项查看

使用 @testing-library/jest-dom 后增加的 matchers 查看

借助 @testing-library/user-event 实现的事件 查看

一个测试案例

import '@testing-library/jest-dom';
import {render, cleanup, } from '@testing-library/react';
import userEvent from "@testing-library/user-event";
import React from 'react';
import Button from "../Button";

/**
 * 在每个测试用例完成后执行 react ummount 卸载程序
 * 在 jest 中这个行为会默认加上,不需要我们手动加上
 * */
// afterEach(cleanup);

describe('测试 React 组件', () => {
    test('测试组件渲染', () => {
        /**
         * 使用 render 将组件渲染到 dom 上,
         * 其返回值,包含 DOM Testing Library 中的 queries 查询方法
         * */
        const result = render(<Button appearance="primary" >TEXT</Button>);
        /**
         * debug() 用来查看渲染出的 dom 是什么样的结构 (html), 主要用来辅助测试
         * 执行 test 可以在控制台看到, 其渲染出的时普通的 html ,复杂的 react 状态等并没有展示
         * 由此可见,RTL 不关心组件的编写方式,它只关心最后生成的 DOM 树是否符合要求
         */
        result.debug();
        /**
         * 通过无障碍属性 role='button' 来查找元素,name 则表示 aria-label 属性
         * */
        expect(result.getByRole('button')).toBeInTheDocument();
        /**
         * 通过文本内容来查找元素,但是尽量不要用在 div span p 等元素的查找上
         * */
        expect(result.getByText('TEXT')).toBeInTheDocument();
        /**
         * 第一个参数也可以写作 正则,用来匹配
         * */
        expect(result.getByText(/^TEX/)).toBeInTheDocument();
        /**
         * 可以使用其他的 matchers https://github.com/testing-library/jest-dom#custom-matchers
         * */
        expect(result.getByText(/^TEX/)).toHaveClass('yufu-btn-primary');

        const input = render(
            <input type="text" alt="testAlt" placeholder="testPlaceholder" data-testid="TTTT" />
        )
        /**
         * 通过 alt 属性查找 img area input 等元素
         * */
        expect(input.getByAltText('testAlt')).toBeInTheDocument();
        /**
         *  通过 placeholder 查询元素
         * */
        expect(input.getByPlaceholderText('testPlaceholder')).toBeInTheDocument();
        /**
         * queryBy... 用来断言不存在的"预期"
         * */
        expect(input.queryByPlaceholderText('test')).not.toBeInTheDocument();
        /**
         * 通过 testid 查询元素
         * */
        expect(input.getByTestId('TTTT')).toBeInTheDocument();
        /**
         * 借助 container 使用DOM 查询元素
         * */
        expect(input.container.querySelector('input[type="text"]')).toBeInTheDocument();


        /**
         * 借助 @testing-library/user-event 来测试事件
         * */
        userEvent.type(input.getByTestId('TTTT'), 'something'); // 触发事件
        expect(input.getByTestId('TTTT')).toHaveValue('something'); // 验证事件结果
    })
})
 类似资料: