如何使用React+jest开展单元测试

具体框架下(cra,umi,antd-pro等)环境的配置略有不同,后续单元测试的写法都是雷同的;环境的配置在面试时不是重点,如何写单测本身才是!

本例以Vite+React+TS为例进行说明。

1. 安装依赖

首先,你需要安装Jest本身以及一些与React和TypeScript相关的依赖。打开终端,切换到你的项目目录,然后运行以下命令:

js 复制代码
npm install --save-dev jest @types/jest ts-jest @testing-library/react @testing-library/jest-dom

这将安装Jest、Jest的TypeScript支持、React的测试库以及必要的类型定义。

  • jest: Jest是一个广泛使用的JavaScript测试框架,它允许你编写和运行测试,包括单元测试、集成测试和端到端测试。Jest提供了测试运行器、断言库、和模拟支持,使得测试JavaScript代码变得简单快捷。
  • @types/jest: 这是Jest的类型定义包,用于TypeScript。由于Jest本身是用JavaScript编写的,@types/jest提供了所有Jest函数和对象的TypeScript类型定义。这使得在使用TypeScript编写测试时,你可以获得类型检查和代码自动完成等功能。
  • ts-jest: ts-jest是一个Jest插件,用于处理TypeScript代码。它允许Jest直接运行TypeScript文件,而不需要先将它们转换为JavaScript。ts-jest提供了TypeScript的编译和配置支持,确保测试代码能够正确地处理TypeScript的特性。
  • @testing-library/react: 这是React Testing Library的核心包,它提供了一套用于测试React组件的实用函数。React Testing Library的目标是模拟用户在使用应用时的行为,而不是测试组件的内部状态,从而鼓励更好的测试实践。它提供了查询DOM元素、触发事件等功能,使得测试React组件变得更加简单和直观。
  • @testing-library/jest-dom: 这是一个自定义的Jest匹配器集合,用于改善与DOM元素交互的测试。@testing-library/jest-dom提供了一系列额外的DOM元素断言,如.toBeVisible()、.toHaveClass()等,这些断言使得在使用Jest测试DOM元素时更加方便和语义化。

2. 配置Jest

接下来,你需要创建一个Jest配置文件。你可以在项目根目录下创建一个jest.config.js文件,并添加以下配置:

js 复制代码
module.exports = {
  // 使用ts-jest预设,这个预设包含了处理TypeScript文件所需的所有配置
  preset: 'ts-jest',

  // 设置测试环境为jsdom,jsdom模拟了一个浏览器环境,允许在Node环境下运行浏览器特定的API
  testEnvironment: 'jsdom',

  // 在每个测试文件运行之后,立即执行指定的脚本文件,这里是项目根目录下的src/setupTests.ts
  // 通常用于全局的测试设置,比如配置enzyme或jest-dom等
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],

  // 配置模块名称映射,用于将导入语句中的别名映射到实际的文件路径
  moduleNameMapper: {
    // 将@components别名映射到src/components目录
    // 这样在测试中可以使用@components/xxx来引入组件
    // 在tsconfig.json中也需要进行对称的配置
    '^@components/(.*)$': '<rootDir>/src/components/$1',
  },

  // 配置文件转换规则,告诉Jest如何处理项目中的不同类型的文件
  transform: {
    // 使用ts-jest处理.ts和.tsx文件
    // 这允许Jest理解TypeScript语法并将其转换为JavaScript
    '^.+\\.(ts|tsx)$': 'ts-jest',

    // 使用babel-jest处理.js和.jsx文件
    // 这允许Jest通过Babel转换这些文件,支持ES6语法和React JSX
    '^.+\\.(js|jsx)$': 'babel-jest',
  },

  // 指定Jest在转换过程中应该忽略的文件模式
  // 这里配置为忽略node_modules目录下的所有文件,这些文件通常不需要转换
  transformIgnorePatterns: ['<rootDir>/node_modules/'],
};

这个配置做了几件事情:

  • 使用ts-jest预设来处理TypeScript文件。

  • 设置测试环境为jsdom,这对于模拟浏览器环境的React测试很有用。

  • 指定了一个setupFilesAfterEnv配置,这允许你在每次测试之前自动加载一些配置或全局mocks。

  • moduleNameMapper用于解析模块别名,这在你的项目中使用了如Webpack别名时非常有用。

  • transform配置告诉Jest如何处理.ts、.tsx、.js和.jsx文件。

3. 设置测试启动文件

在src目录下创建一个setupTests.ts文件,用于配置或添加一些在测试之前需要运行的代码。例如,你可以在这里导入@testing-library/jest-dom以扩展Jest的断言库:

js 复制代码
import '@testing-library/jest-dom';

4. 编写测试

现在可以开始编写测试了。

测试普通函数

此处我们写一个缓存执行结果的闭包

src/utils/functionalUtil.ts

js 复制代码
export function cacheIt(fn: Function) {
    const cache = new Map<string, any>();

    return function (...args: any[]) {
        const key = JSON.stringify(args);
        if (!cache.has(key)) {
            const result = fn.apply(null, args);
            cache.set(key, result);
            return result;
        }
        return cache.get(key);
    }
}

使用jest断言语法编写测试脚本

tests/utils/functionalUtil.test.ts

js 复制代码
// 从项目的utils/functionalUtil模块中导入cacheIt函数
import { cacheIt } from '@/utils/functionalUtil';

// 使用describe定义一组测试用例,这组测试用例的目的是测试cacheIt函数
describe('cacheIt', () => {

    // 定义一个测试用例,测试cacheIt是否能正确缓存函数结果
    it('should cache and return the result for the same arguments', () => {
        // 使用jest.fn创建一个模拟函数add,模拟一个加法操作
        const add = jest.fn((a: number, b: number) => a + b);

        // 使用cacheIt函数对add函数进行缓存处理
        const cachedAdd = cacheIt(add);

        // 调用cachedAdd函数两次,传入相同的参数(2, 3),并期望返回值为5
        expect(cachedAdd(2, 3)).toBe(5);
        expect(cachedAdd(2, 3)).toBe(5);

        // 验证add函数只被实际调用了一次,因为第二次调用时结果应该是从缓存中获取的
        expect(add).toHaveBeenCalledTimes(1);

        // 再次调用cachedAdd函数,但这次传入不同的参数(3, 4),并期望返回值为7
        expect(cachedAdd(3, 4)).toBe(7);

        // 验证add函数此时被调用了两次,因为传入了新的参数组合
        expect(add).toHaveBeenCalledTimes(2);
    });

    // 定义另一个测试用例,测试对于相同的参数是否总是返回缓存的结果
    it('should return cached result for the same arguments called multiple times', () => {
        // 使用jest.fn创建一个模拟函数multiply,模拟一个乘法操作
        const multiply = jest.fn((a: number, b: number) => a * b);

        // 使用cacheIt函数对multiply函数进行缓存处理
        const cachedMultiply = cacheIt(multiply);

        // 分别两次调用cachedMultiply函数,传入相同的参数(4, 5)
        const firstCallResult = cachedMultiply(4, 5);
        const secondCallResult = cachedMultiply(4, 5);

        // 验证两次调用的返回值都为20
        expect(firstCallResult).toBe(20);
        expect(secondCallResult).toBe(20);
        
        // 验证multiply函数只被实际调用了一次,因为第二次调用时结果应该是从缓存中获取的
        expect(multiply).toHaveBeenCalledTimes(1);
    });
});

这组测试用例通过模拟函数和cacheIt函数的组合使用,验证了cacheIt能够正确地缓存函数调用结果,并在相同参数的后续调用中返回缓存的结果,从而减少实际函数调用的次数。

5. 运行测试

最后,你需要在package.json中添加一个脚本来运行测试:

json 复制代码
{
  "scripts": {
    "test": "jest"
  }
}

现在,你可以通过运行以下命令来执行你的测试:

js 复制代码
npm test

这些步骤应该帮助你在使用Vite、React和TypeScript的项目中集成Jest进行单元测试。记得根据你的项目需求调整配置和测试。

6. 测试网络请求

编写一个模拟异步返回数据的API函数

src/apis/modelOne.ts

js 复制代码
export async function fetchData(url: string): Promise<any> {
    try {
        const response = await fetch(url);
        if (!response.ok) {
            throw new Error('Network response was not ok');
        }
        const data = await response.json();
        return data;
    } catch (error) {
        throw new Error(`Fetching data failed: ${error}`);
    }
}

编写测试脚本

tests/apis/modelOne.test.ts

js 复制代码
import { fetchData } from '@/apis/modelOne'; // 从modelOne模块导入fetchData函数

// 使用jest来模拟全局的fetch
global.fetch = jest.fn(() =>
    Promise.resolve({
        ok: true, // 模拟fetch请求成功返回
        json: () => Promise.resolve({ message: 'Success' }), // 模拟返回的JSON数据
    })
) as jest.Mock; // 将模拟的fetch函数强制类型转换为jest.Mock类型

beforeEach(() => {
    // 在每个测试用例运行之前清除模拟的调用和实例
    (fetch as jest.Mock).mockClear(); // 清除fetch模拟函数的调用记录
});

test('fetchData returns data on successful fetch', async () => {
    const data = await fetchData('https://example.com/data'); // 调用fetchData函数

    expect(fetch).toHaveBeenCalledTimes(1); // 验证fetch是否被调用了一次
    expect(fetch).toHaveBeenCalledWith('https://example.com/data'); // 验证fetch是否用正确的URL被调用
    expect(data).toEqual({ message: 'Success' }); // 验证fetchData函数的返回值是否符合预期
});

test('fetchData throws an error when fetch fails', async () => {
    (fetch as jest.Mock).mockRejectedValue(new Error('Failed to fetch')); // 模拟fetch请求失败

    await expect(fetchData('https://example.com/data')).rejects.toThrow('Fetching data failed: Error: Failed to fetch'); // 验证当fetch失败时,fetchData函数是否按预期抛出错误
});

这个测试文件主要做了两件事:1. 使用jest.fn()模拟全局的fetch函数,以便在不发出真实网络请求的情况下测试fetchData函数的行为。模拟的fetch函数可以根据需要返回成功或失败的响应。2. 定义了两个测试用例:

  • 第一个测试用例验证当fetch成功时,fetchData函数是否正确返回数据。

  • 第二个测试用例验证当fetch失败时,fetchData函数是否抛出了预期的错误。

7. 测试同步组件

src/components/Hello.tsx

js 复制代码
export default function Hello({ name }: { name: string }) {
    return (
        <h1>Hello, {name}! </h1>
    )
}

tests/components/Hello.test.tsx

js 复制代码
// 从@testing-library/react库中导入render和screen工具
import { render, screen } from '@testing-library/react';
// 从项目的components目录中导入Hello组件
import Hello from '@components/Hello';

// 定义一个测试用例,测试名称为'renders hello message'
test('renders hello message', () => {

    // 使用render函数渲染Hello组件,并传入props,这里传入的name为"world"
    render(<Hello name="world" />);
    
    // 使用screen.getByText查询函数来查找页面上的文本内容
    // 这里使用正则表达式/i来忽略大小写,匹配文本"hello, world!"
    const helloElement = screen.getByText(/hello, world!/i);
    
    // 使用expect函数和toBeInTheDocument匹配器来断言
    // 检查helloElement是否成功渲染在了文档中
    expect(helloElement).toBeInTheDocument();
});

这个测试用例的目的是验证Hello组件是否能够根据传入的name prop正确渲染出"hello, world!"这个消息。通过@testing-library/react提供的render函数来渲染组件,并使用screen.getByText来查询渲染结果中是否包含了期望的文本内容。最后,使用expect和toBeInTheDocument来断言查询到的元素确实存在于文档中,从而验证组件的渲染行为。

8. 测试异步数据组件

src/components/MessageFetcher.tsx

js 复制代码
import /* React, */ { useState } from 'react';

export const MessageFetcher = () => {
    const [message, setMessage] = useState('');

    const fetchMessage = async () => {
        try {
            const response = await fetch('https://api.example.com/message');
            const data = await response.json();
            setMessage(data.message);
        } catch (error) {
            console.error('Fetching message failed:', error);
            setMessage('Error fetching message');
        }
    };

    return (
        <div>
            <button onClick={fetchMessage}>Fetch Message</button>
            {message && <p>{message}</p>}
        </div>
    );
};

tests/components/MessageFetcher.test.tsx

js 复制代码
// 从@testing-library/react库中导入render, screen, fireEvent, 和 waitFor工具
import { render, screen, fireEvent, waitFor } from '@testing-library/react';

// 导入@testing-library/jest-dom以获得额外的jest断言方法
import '@testing-library/jest-dom';

// 从项目的components目录中导入MessageFetcher组件
import { MessageFetcher } from '@components/MessageFetcher';

// 使用jest.fn()模拟全局的fetch函数
global.fetch = jest.fn(() =>
    Promise.resolve({
        json: () => Promise.resolve({ message: 'Hello from the API' }), // 模拟fetch请求成功,并返回一个对象,该对象包含一个json方法,json方法返回一个解析为包含特定消息的对象的Promise
    })
) as jest.Mock; // 将模拟的fetch函数强制类型转换为jest.Mock类型

// 使用describe函数定义一组相关的测试
describe('MessageFetcher', () => {
    beforeEach(() => {
        // 在每个测试用例运行之前,使用mockClear方法清除fetch模拟函数的调用记录和实例
        (fetch as jest.Mock).mockClear();
    });

    it('fetches and displays the message', async () => {
        // 使用render函数渲染MessageFetcher组件
        render(<MessageFetcher />);

        // 使用fireEvent.click模拟用户点击操作,触发获取消息的按钮
        fireEvent.click(screen.getByText('Fetch Message'));

        // 使用waitFor异步等待,直到期望的断言通过
        await waitFor(
            // 使用expect函数和toBeInTheDocument断言方法来检查页面上是否成功显示了API返回的消息
            () => expect(screen.getByText('Hello from the API')).toBeInTheDocument()
        );

        // 检查fetch是否被准确地调用了一次
        expect(fetch).toHaveBeenCalledTimes(1);
    });
});

这个测试文件主要做了以下几件事:

    1. 使用jest.fn()模拟全局的fetch函数,以便在不发出真实网络请求的情况下测试组件的行为。模拟的fetch函数被配置为返回一个成功的响应,其中包含一个消息。
    1. 使用describe和it定义测试套件和测试用例,beforeEach用于设置每个测试用例之前的初始条件,这里是清除fetch模拟的调用记录。
    1. 在测试用例中,首先渲染MessageFetcher组件,然后模拟用户点击操作以触发消息的获取,最后验证是否成功获取并显示了消息,以及fetch函数是否被正确调用。

批量执行效果

相关推荐
Dnelic-2 小时前
【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录
android·junit·单元测试·android studio·自学笔记
世间万物皆对象8 小时前
Spring Boot核心概念:日志管理
java·spring boot·单元测试
Dnelic-19 小时前
解决 Android 单元测试 No tests found for given includes:
android·junit·单元测试·问题记录·自学笔记
岳哥i1 天前
前端项目接入单元测试手册
前端·单元测试
qq_433716951 天前
Selenium+Pytest自动化测试框架 ------ 禅道实战
自动化测试·软件测试·selenium·单元测试·pytest·接口测试·压力测试
Dreams°1232 天前
【大数据测试ETL:从0-1实战详细教程】
大数据·数据仓库·python·单元测试·etl
敲代码敲到头发茂密2 天前
怎么做好白盒测试?
java·数据库·mysql·算法·单元测试·模块测试·测试覆盖率
Dreams°1235 天前
【大数据测试HDFS + Flask详细教程与实例】
大数据·功能测试·hdfs·单元测试·flask
19组清风6 天前
对于模块动态加载,Vite 内部做了哪些优化
前端·vite·前端工程化
CSXB996 天前
三十八、Python(pytest框架-上)
python·功能测试·测试工具·单元测试·pytest