当前位置: 首页 > news >正文

06-高级模式与实战项目——09. 单元测试 - Jest

09. 单元测试 - Jest

概述

Jest 是 Facebook 开源的 JavaScript 测试框架,广泛用于 React 应用的单元测试。它提供测试运行器、断言库、Mock 功能、代码覆盖率等一站式解决方案。

维度内容
What使用 Jest 测试 JavaScript/React 代码的单元
Why确保代码正确性,提高代码质量
When测试函数、工具类、自定义 Hooks
Where__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)); // 10

4.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('test@example.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、rejects
Mockjest.fn()、jest.mock()

记忆口诀

Jest 测试框架强,describe 分组 test 写
expect 断言各种值,异步测试要等待
Mock 模拟外部依赖,代码覆盖率不用愁


8. 相关资源

  • Jest 官方文档
  • React Testing Library

http://www.jsqmd.com/news/1138355/

相关文章:

  • 储能沙盘模型BMS功能模拟控制系统设计与实现:电压监测、均衡管理与故障保护的STM32方案
  • npx --yes localtunnel --port 3000不安装任何依赖的情况下将端口暴露在公开url上
  • Scikit-learn 1.5.0 实战:5行代码构建KNN分类器,准确率超95%(附数据集)
  • Ubuntu 22.04 + CUDA 12.1 深度学习环境配置:3步完成PyTorch 2.0与TensorFlow 2.10共存
  • EhViewer安全架构深度解析:多层次防护机制的技术实现
  • Windows 11 安装 Claude Code:配置国产模型(DeepSeek)、无需解决网络问题
  • StegaStamp 2019 CVPR 实战复现:PyTorch 1.13 环境配置与 100 位消息嵌入测试
  • 2026年国产大模型API价格横评:DeepSeek/Kimi/GLM/Qwen哪家最便宜?附迁移避坑指南
  • 小黄鸭(Lossless Scaling)ai补帧神器配置方法与安装教程
  • CTF ECDH密钥协商漏洞攻击 Writeup
  • U-Net 跳跃连接深度解析:Concat 与 Add 操作的 3 种实现与性能影响
  • 【单片机毕业设计】基于 STM32 的智能护眼台灯监测控制系统设计,基于单片机的坐姿矫正与久坐提醒智能台灯设计(018401)
  • 乐达,乐运助手app官方下载
  • 2026 电商 AI 图文 + 动态素材完整接单市场价(闲鱼 / 小红书 / 淘宝美工真实行情)
  • Excel复选框本质解析:布尔逻辑驱动的数据交互设计
  • 给 AI 用的代码索引器-技术篇
  • Nacos漏洞利用工具V3.0.5:安全评估实践与工程化实现
  • 嵌入式Linux网络通信知识框架-以太网、WiFi、4G与5G
  • 岗位薪资范围太宽不知道值多少?3个定位方法
  • UI自动化性能优化全链路实战:从脚本编码到分布式架构
  • Novel-Downloader:200+网站智能解析的小说下载神器
  • 德州中央空调维保怎么选?本地服务避坑指南
  • 从分立到集成:对比4款移动电源SOC与ME4057+MCU方案的成本与PCB面积
  • 影刀RPA新手教程:知乎自动化完全指南问题监控与回答采集实战
  • RAG 多路召回:让检索命中率从 60% 干到 85% 的 4 层优化
  • L 型 / 三折 / 四折 LED 异形大屏裸眼 3D 动画制作有哪些硬性技术要求?行业标准与避坑全解
  • Shielding 与 Spacing:2种串扰抑制方案在 7nm 工艺下的 PPA 对比
  • 新手 AI 电商图文 + 动态素材接单 完整落地操作步骤(从 0 到成交,一步不漏)
  • 如何用Marketch插件将Sketch设计稿自动转换为HTML与CSS:5分钟快速上手指南
  • 联邦学习 3D-ResUNet 实战:6大洲71中心胶质瘤分割,DSC提升23%