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