# react-tester **Repository Path**: steveouyang/react-tester ## Basic Information - **Project Name**: react-tester - **Description**: react+jest进行单元测试 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-03-21 - **Last Updated**: 2024-03-22 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README 具体框架下(cra,umi,antd-pro等)环境的配置略有不同,后续单元测试的写法都是雷同的;环境的配置在面试时不是重点,如何写单测本身才是! 本例以`Vite+React+TS`为例进行说明。 [源码地址](https://gitee.com/steveouyang/react-tester) ### 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: ['/src/setupTests.ts'], // 配置模块名称映射,用于将导入语句中的别名映射到实际的文件路径 moduleNameMapper: { // 将@components别名映射到src/components目录 // 这样在测试中可以使用@components/xxx来引入组件 // 在tsconfig.json中也需要进行对称的配置 '^@components/(.*)$': '/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: ['/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(); 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进行单元测试。记得根据你的项目需求调整配置和测试。 ![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5a98a2a3979e492e8872ea8c3f2d2ba7~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1056&h=499&s=66397&e=png&b=181818) ### 6. 测试网络请求 编写一个模拟异步返回数据的API函数 *src/apis/modelOne.ts* ```js export async function fetchData(url: string): Promise { 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 (

Hello, {name}!

) } ``` *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(); // 使用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 (
{message &&

{message}

}
); }; ``` *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(); // 使用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函数被配置为返回一个成功的响应,其中包含一个消息。 - 2. 使用describe和it定义测试套件和测试用例,beforeEach用于设置每个测试用例之前的初始条件,这里是清除fetch模拟的调用记录。 - 3. 在测试用例中,首先渲染MessageFetcher组件,然后模拟用户点击操作以触发消息的获取,最后验证是否成功获取并显示了消息,以及fetch函数是否被正确调用。 ### 批量执行效果 ![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ca16219b5ccc4943b139df2c501cb2e7~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1170&h=784&s=146737&e=png&b=181818)