09. 单元测试 - Jest概述Jest 是 Facebook 开源的 JavaScript 测试框架广泛用于 React 应用的单元测试。它提供测试运行器、断言库、Mock 功能、代码覆盖率等一站式解决方案。维度内容What使用 Jest 测试 JavaScript/React 代码的单元Why确保代码正确性提高代码质量When测试函数、工具类、自定义 HooksWhere__tests__目录或.test.js文件Who需要保证代码质量的开发者Howtest(adds 1 2 to equal 3, () { expect(add(1, 2)).toBe(3) })1. Jest 基础1.1 安装配置npminstall--save-dev jest types/jest// package.json{scripts:{test:jest,test:watch:jest --watch,test:coverage:jest --coverage}}1.2 基本测试结构// math.js export function add(a, b) { return a b; } export function subtract(a, b) { return a - b; } export function multiply(a, b) { return a * b; } export function divide(a, b) { if (b 0) throw new Error(除数不能为0); return a / b; } // math.test.js import { add, subtract, multiply, divide } from ./math; describe(数学函数, () { test(add 函数: 1 2 3, () { expect(add(1, 2)).toBe(3); }); test(subtract 函数: 5 - 3 2, () { expect(subtract(5, 3)).toBe(2); }); test(multiply 函数: 2 * 3 6, () { expect(multiply(2, 3)).toBe(6); }); test(divide 函数: 6 / 2 3, () { expect(divide(6, 2)).toBe(3); }); test(divide 函数: 除以0抛出错误, () { expect(() divide(6, 0)).toThrow(除数不能为0); }); });2. 常用断言2.1 相等性断言test(相等性断言, () { // 基本相等 expect(1 1).toBe(2); // 对象相等深度比较 expect({ name: 张三 }).toEqual({ name: 张三 }); // 不相等 expect(1 1).not.toBe(3); // 严格相等SameValue expect(NaN).toBeNaN(); expect(0).not.toBe(-0); });2.2 真值断言test(真值断言, () { // 真值 expect(true).toBeTruthy(); expect(1).toBeTruthy(); expect(hello).toBeTruthy(); // 假值 expect(false).toBeFalsy(); expect(0).toBeFalsy(); expect().toBeFalsy(); expect(null).toBeFalsy(); expect(undefined).toBeFalsy(); // 特定值 expect(null).toBeNull(); expect(undefined).toBeUndefined(); expect(hello).toBeDefined(); });2.3 数值断言test(数值断言, () { expect(5).toBeGreaterThan(3); // expect(5).toBeGreaterThanOrEqual(5); // expect(3).toBeLessThan(5); // expect(3).toBeLessThanOrEqual(3); // expect(0.1 0.2).toBeCloseTo(0.3); // 浮点数 });2.4 数组和对象断言test(数组和对象断言, () { // 数组 expect([1, 2, 3]).toContain(2); expect([1, 2, 3]).toHaveLength(3); // 对象 expect({ name: 张三, age: 18 }).toHaveProperty(name); expect({ name: 张三, age: 18 }).toHaveProperty(age, 18); // 字符串 expect(hello world).toContain(world); expect(hello world).toMatch(/world/); });3. 异步测试3.1 Promise 测试// api.js export function fetchUser(id) { return fetch(/api/users/${id}).then(res res.json()); } // api.test.js import { fetchUser } from ./api; // 使用 async/await test(fetchUser 返回用户数据, async () { const user await fetchUser(1); expect(user).toHaveProperty(id, 1); }); // 使用 resolves test(fetchUser 返回用户数据 (resolves), () { return expect(fetchUser(1)).resolves.toHaveProperty(id, 1); }); // 测试错误 test(fetchUser 失败时抛出错误, async () { await expect(fetchUser(999)).rejects.toThrow(); });3.2 回调测试// callback.js export function fetchData(callback) { setTimeout(() { callback({ data: success }); }, 100); } // callback.test.js import { fetchData } from ./callback; test(fetchData 调用回调, (done) { fetchData((data) { expect(data).toEqual({ data: success }); done(); // 等待回调完成 }); });4. Mock 功能4.1 函数 Mock// 创建 Mock 函数 test(Mock 函数, () { const mockFn jest.fn(); mockFn(hello, world); mockFn(again); // 调用断言 expect(mockFn).toHaveBeenCalled(); expect(mockFn).toHaveBeenCalledTimes(2); expect(mockFn).toHaveBeenCalledWith(hello, world); expect(mockFn).toHaveBeenLastCalledWith(again); }); // Mock 返回值 const mockFn jest.fn() .mockReturnValueOnce(first) .mockReturnValueOnce(second) .mockReturnValue(default); console.log(mockFn()); // first console.log(mockFn()); // second console.log(mockFn()); // default // Mock 实现 const mockFn jest.fn((x) x * 2); console.log(mockFn(5)); // 104.2 模块 Mock// __mocks__/axios.js export default { get: jest.fn(() Promise.resolve({ data: { users: [] } })), post: jest.fn(() Promise.resolve({ data: { success: true } })), }; // userService.test.js import axios from axios; import { getUsers, createUser } from ./userService; jest.mock(axios); test(getUsers 返回用户列表, async () { const mockUsers [{ id: 1, name: 张三 }]; axios.get.mockResolvedValue({ data: mockUsers }); const users await getUsers(); expect(users).toEqual(mockUsers); expect(axios.get).toHaveBeenCalledWith(/api/users); }); test(createUser 创建用户, async () { const newUser { name: 李四 }; axios.post.mockResolvedValue({ data: { id: 2, ...newUser } }); const user await createUser(newUser); expect(user).toHaveProperty(id, 2); });4.3 时间 Mock// 使用假计时器 jest.useFakeTimers(); test(延迟执行, () { const callback jest.fn(); setTimeout(callback, 1000); jest.advanceTimersByTime(500); expect(callback).not.toHaveBeenCalled(); jest.advanceTimersByTime(500); expect(callback).toHaveBeenCalled(); });5. 测试 React 组件5.1 工具函数测试// utils/format.js export function formatDate(date) { return new Date(date).toLocaleDateString(zh-CN); } export function formatPrice(price) { return ¥${price.toFixed(2)}; } export function validateEmail(email) { return /\S\S\.\S/.test(email); } // utils/format.test.js import { formatDate, formatPrice, validateEmail } from ./format; describe(格式化函数, () { test(formatDate 格式化日期, () { expect(formatDate(2024-01-15)).toBe(2024/1/15); }); test(formatPrice 格式化价格, () { expect(formatPrice(19.9)).toBe(¥19.90); expect(formatPrice(100)).toBe(¥100.00); }); test(validateEmail 验证邮箱, () { expect(validateEmail(testexample.com)).toBe(true); expect(validateEmail(invalid)).toBe(false); }); });5.2 自定义 Hook 测试// hooks/useCounter.js import { useState, useCallback } from react; export function useCounter(initialValue 0) { const [count, setCount] useState(initialValue); const increment useCallback(() setCount(c c 1), []); const decrement useCallback(() setCount(c c - 1), []); const reset useCallback(() setCount(initialValue), [initialValue]); return { count, increment, decrement, reset }; } // hooks/useCounter.test.js import { renderHook, act } from testing-library/react; import { useCounter } from ./useCounter; describe(useCounter, () { test(初始值为 0, () { const { result } renderHook(() useCounter()); expect(result.current.count).toBe(0); }); test(可以设置初始值, () { const { result } renderHook(() useCounter(10)); expect(result.current.count).toBe(10); }); test(increment 增加计数, () { const { result } renderHook(() useCounter()); act(() { result.current.increment(); }); expect(result.current.count).toBe(1); }); test(decrement 减少计数, () { const { result } renderHook(() useCounter(5)); act(() { result.current.decrement(); }); expect(result.current.count).toBe(4); }); test(reset 重置计数, () { const { result } renderHook(() useCounter(10)); act(() { result.current.increment(); result.current.increment(); result.current.reset(); }); expect(result.current.count).toBe(10); }); });6. 完整示例// services/todoService.js const API_URL https://api.example.com/todos; export async function fetchTodos() { const response await fetch(API_URL); if (!response.ok) throw new Error(获取失败); return response.json(); } export async function createTodo(todo) { const response await fetch(API_URL, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(todo), }); if (!response.ok) throw new Error(创建失败); return response.json(); } export async function updateTodo(id, updates) { const response await fetch(${API_URL}/${id}, { method: PATCH, headers: { Content-Type: application/json }, body: JSON.stringify(updates), }); if (!response.ok) throw new Error(更新失败); return response.json(); } export async function deleteTodo(id) { const response await fetch(${API_URL}/${id}, { method: DELETE }); if (!response.ok) throw new Error(删除失败); return true; } // services/todoService.test.js import { fetchTodos, createTodo, updateTodo, deleteTodo } from ./todoService; global.fetch jest.fn(); describe(Todo Service, () { beforeEach(() { fetch.mockClear(); }); describe(fetchTodos, () { test(成功获取 todos, async () { const mockTodos [{ id: 1, title: 测试, completed: false }]; fetch.mockResolvedValue({ ok: true, json: async () mockTodos, }); const todos await fetchTodos(); expect(todos).toEqual(mockTodos); expect(fetch).toHaveBeenCalledWith(https://api.example.com/todos); }); test(获取失败时抛出错误, async () { fetch.mockResolvedValue({ ok: false }); await expect(fetchTodos()).rejects.toThrow(获取失败); }); }); describe(createTodo, () { test(成功创建 todo, async () { const newTodo { title: 新任务, completed: false }; const createdTodo { id: 1, ...newTodo }; fetch.mockResolvedValue({ ok: true, json: async () createdTodo, }); const result await createTodo(newTodo); expect(result).toEqual(createdTodo); expect(fetch).toHaveBeenCalledWith( https://api.example.com/todos, expect.objectContaining({ method: POST }) ); }); }); describe(updateTodo, () { test(成功更新 todo, async () { const updates { completed: true }; const updatedTodo { id: 1, title: 测试, completed: true }; fetch.mockResolvedValue({ ok: true, json: async () updatedTodo, }); const result await updateTodo(1, updates); expect(result).toEqual(updatedTodo); expect(fetch).toHaveBeenCalledWith( https://api.example.com/todos/1, expect.objectContaining({ method: PATCH }) ); }); }); describe(deleteTodo, () { test(成功删除 todo, async () { fetch.mockResolvedValue({ ok: true }); const result await deleteTodo(1); expect(result).toBe(true); expect(fetch).toHaveBeenCalledWith( https://api.example.com/todos/1, expect.objectContaining({ method: DELETE }) ); }); test(删除失败时抛出错误, async () { fetch.mockResolvedValue({ ok: false }); await expect(deleteTodo(1)).rejects.toThrow(删除失败); }); }); });7. 总结核心要点要点说明测试结构describe test/ it断言expect matcher异步测试async/await、resolves、rejectsMockjest.fn()、jest.mock()记忆口诀Jest 测试框架强describe 分组 test 写expect 断言各种值异步测试要等待Mock 模拟外部依赖代码覆盖率不用愁8. 相关资源Jest 官方文档React Testing Library