news 2026/5/14 7:56:09

单元测试最佳实践:编写可测试的前端代码

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
单元测试最佳实践:编写可测试的前端代码

单元测试最佳实践:编写可测试的前端代码

前言

大家好,我是cannonmonster01!今天我们来聊聊单元测试的最佳实践。

想象一下,你是一个厨师,在做出一道菜之后,你需要尝一尝味道如何。如果味道不好,你可以及时调整配方。单元测试就像是这个尝味道的过程,它可以帮助我们及时发现代码中的问题。

一个好的单元测试应该像一个专业的美食评论家,能够准确地判断代码的质量!

单元测试核心概念

什么是单元测试

单元测试是对软件中的最小可测试单元进行检查和验证。在前端中,通常是对单个函数、组件或模块进行测试。

单元测试的特点

特点描述
隔离性每个测试应该独立,不依赖其他测试
可重复性相同的输入应该产生相同的输出
快速性单元测试应该快速执行
自动化测试应该可以自动运行

测试覆盖率

测试覆盖率是衡量测试完整性的指标,包括:

  • 语句覆盖率:有多少代码被执行
  • 分支覆盖率:有多少条件分支被测试
  • 函数覆盖率:有多少函数被调用
  • 行覆盖率:有多少行代码被执行

单元测试实战

实战1:测试纯函数

// utils.js export function add(a, b) { return a + b; } export function capitalize(str) { if (!str) return ''; return str.charAt(0).toUpperCase() + str.slice(1); } export function formatDate(date) { return new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: 'long', day: 'numeric', }).format(date); }
// utils.test.js import { add, capitalize, formatDate } from './utils'; describe('Utils', () => { describe('add', () => { it('should add two numbers', () => { expect(add(1, 2)).toBe(3); expect(add(-1, 1)).toBe(0); expect(add(0, 0)).toBe(0); }); }); describe('capitalize', () => { it('should capitalize the first letter', () => { expect(capitalize('hello')).toBe('Hello'); expect(capitalize('world')).toBe('World'); }); it('should return empty string for empty input', () => { expect(capitalize('')).toBe(''); }); it('should handle null and undefined', () => { expect(capitalize(null)).toBe(''); expect(capitalize(undefined)).toBe(''); }); }); describe('formatDate', () => { it('should format date correctly', () => { const date = new Date('2024-01-15'); expect(formatDate(date)).toBe('2024年1月15日'); }); }); });

实战2:测试React组件

// Button.jsx import React from 'react'; function Button({ children, onClick, disabled }) { return ( <button onClick={onClick} disabled={disabled} className={`btn ${disabled ? 'btn-disabled' : 'btn-primary'}`} > {children} </button> ); } export default Button;
// Button.test.jsx import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import Button from './Button'; describe('Button', () => { it('should render children', () => { render(<Button>Click me</Button>); expect(screen.getByText('Click me')).toBeInTheDocument(); }); it('should call onClick when clicked', () => { const handleClick = jest.fn(); render(<Button onClick={handleClick}>Click me</Button>); fireEvent.click(screen.getByText('Click me')); expect(handleClick).toHaveBeenCalledTimes(1); }); it('should be disabled when disabled prop is true', () => { render(<Button disabled>Click me</Button>); expect(screen.getByText('Click me')).toBeDisabled(); }); it('should not call onClick when disabled', () => { const handleClick = jest.fn(); render(<Button onClick={handleClick} disabled>Click me</Button>); fireEvent.click(screen.getByText('Click me')); expect(handleClick).not.toHaveBeenCalled(); }); it('should have correct className', () => { render(<Button>Click me</Button>); expect(screen.getByText('Click me')).toHaveClass('btn', 'btn-primary'); render(<Button disabled>Click me</Button>); expect(screen.getByText('Click me')).toHaveClass('btn', 'btn-disabled'); }); });

实战3:测试自定义Hook

// useCounter.js import { useState, useCallback } from 'react'; export function useCounter(initialValue = 0) { const [count, setCount] = useState(initialValue); const increment = useCallback(() => { setCount(prev => prev + 1); }, []); const decrement = useCallback(() => { setCount(prev => prev - 1); }, []); const reset = useCallback(() => { setCount(initialValue); }, [initialValue]); return { count, increment, decrement, reset }; }
// useCounter.test.js import { renderHook, act } from '@testing-library/react'; import { useCounter } from './useCounter'; describe('useCounter', () => { it('should initialize with initial value', () => { const { result } = renderHook(() => useCounter(5)); expect(result.current.count).toBe(5); }); it('should increment count', () => { const { result } = renderHook(() => useCounter()); act(() => { result.current.increment(); }); expect(result.current.count).toBe(1); }); it('should decrement count', () => { const { result } = renderHook(() => useCounter(5)); act(() => { result.current.decrement(); }); expect(result.current.count).toBe(4); }); it('should reset to initial value', () => { const { result } = renderHook(() => useCounter(5)); act(() => { result.current.increment(); result.current.increment(); }); expect(result.current.count).toBe(7); act(() => { result.current.reset(); }); expect(result.current.count).toBe(5); }); it('should handle zero initial value', () => { const { result } = renderHook(() => useCounter(0)); act(() => { result.current.decrement(); }); expect(result.current.count).toBe(-1); }); });

实战4:测试异步函数

// api.js export async function fetchUser(userId) { const response = await fetch(`https://api.example.com/users/${userId}`); if (!response.ok) { throw new Error('Failed to fetch user'); } return response.json(); } export async function fetchPosts(page = 1, limit = 10) { const response = await fetch(`https://api.example.com/posts?page=${page}&limit=${limit}`); if (!response.ok) { throw new Error('Failed to fetch posts'); } return response.json(); }
// api.test.js import { fetchUser, fetchPosts } from './api'; jest.mock('node-fetch'); import fetch from 'node-fetch'; describe('API', () => { beforeEach(() => { fetch.mockClear(); }); describe('fetchUser', () => { it('should fetch user data', async () => { const mockUser = { id: '1', name: 'John Doe', email: 'john@example.com' }; fetch.mockResolvedValue({ ok: true, json: async () => mockUser, }); const user = await fetchUser('1'); expect(fetch).toHaveBeenCalledWith('https://api.example.com/users/1'); expect(user).toEqual(mockUser); }); it('should throw error when fetch fails', async () => { fetch.mockResolvedValue({ ok: false, }); await expect(fetchUser('1')).rejects.toThrow('Failed to fetch user'); }); }); describe('fetchPosts', () => { it('should fetch posts with default parameters', async () => { const mockPosts = [{ id: '1', title: 'Post 1' }]; fetch.mockResolvedValue({ ok: true, json: async () => mockPosts, }); const posts = await fetchPosts(); expect(fetch).toHaveBeenCalledWith('https://api.example.com/posts?page=1&limit=10'); expect(posts).toEqual(mockPosts); }); it('should fetch posts with custom parameters', async () => { fetch.mockResolvedValue({ ok: true, json: async () => [], }); await fetchPosts(2, 20); expect(fetch).toHaveBeenCalledWith('https://api.example.com/posts?page=2&limit=20'); }); }); });

单元测试最佳实践

1. 测试结构清晰

describe('Module Name', () => { beforeEach(() => { // 在每个测试前执行 }); afterEach(() => { // 在每个测试后执行 }); describe('Function/Component Name', () => { it('should do something', () => { // 测试代码 }); it('should handle edge case', () => { // 测试边缘情况 }); }); });

2. 使用描述性的测试名称

// 不好的做法 it('works', () => {}); // 好的做法 it('should return the sum of two numbers', () => {}); it('should throw error when input is invalid', () => {});

3. 测试单一职责

// 不好的做法:一个测试做太多事情 it('should update user and fetch posts', async () => { await updateUser(); const posts = await fetchPosts(); expect(posts.length).toBe(5); }); // 好的做法:分开测试 it('should update user', async () => { await updateUser(); }); it('should fetch posts', async () => { const posts = await fetchPosts(); expect(posts.length).toBe(5); });

4. 使用Mock隔离外部依赖

// 不好的做法:依赖真实的API it('should fetch user', async () => { const user = await fetchUser('1'); expect(user.name).toBe('John'); }); // 好的做法:使用Mock it('should fetch user', async () => { fetch.mockResolvedValue({ ok: true, json: async () => ({ id: '1', name: 'John' }), }); const user = await fetchUser('1'); expect(user.name).toBe('John'); });

5. 测试边界情况

describe('capitalize', () => { it('should handle empty string', () => { expect(capitalize('')).toBe(''); }); it('should handle null', () => { expect(capitalize(null)).toBe(''); }); it('should handle undefined', () => { expect(capitalize(undefined)).toBe(''); }); it('should handle single character', () => { expect(capitalize('a')).toBe('A'); }); });

单元测试工具

Jest

Jest是一个流行的JavaScript测试框架,具有以下特点:

  • 内置断言库
  • 内置Mock功能
  • 快照测试
  • 代码覆盖率报告

React Testing Library

React Testing Library是一个用于测试React组件的工具库,强调:

  • 测试用户行为而非实现细节
  • 模拟真实用户交互
  • 易于理解的测试结果

Vitest

Vitest是一个基于Vite的测试框架,特点:

  • 快速启动
  • 热更新
  • 与Vite无缝集成
  • 兼容Jest API

常见问题解答

Q1:单元测试应该覆盖多少代码?

A1:没有固定的标准,但通常建议覆盖率在80%以上。更重要的是测试关键路径和边缘情况。

Q2:如何测试异步代码?

A2:使用async/await,确保测试等待异步操作完成。

Q3:什么时候应该写单元测试?

A3:最好在开发过程中编写测试,遵循TDD(测试驱动开发)原则。

Q4:单元测试应该测试实现细节吗?

A4:不应该。单元测试应该测试行为而非实现细节,这样可以在重构时保持测试稳定。

总结

单元测试是保证代码质量的重要手段。通过遵循最佳实践,我们可以编写更加可靠、可维护的测试代码。记住,一个好的测试应该是隔离的、可重复的、快速的和自动化的。


关注我,每天分享更多前端干货!如果觉得这篇文章对你有帮助,请点赞、收藏、转发三连支持一下!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/14 7:55:41

快手二面:大模型的 Function Call 能力是怎么训练出来的?

&#x1f454;面试官&#xff1a;详细说说大模型的 Function Call 能力是怎么训练出来的&#xff1f; &#x1f64b;‍♂️我&#xff1a;应该就是在预训练阶段&#xff0c;训练语料里包含了一些 API 调用的代码&#xff0c;模型自然就学会了吧&#xff1f; &#x1f454;面试…

作者头像 李华
网站建设 2026/5/14 7:54:07

DNA数据存储:从喷泉码到碱基编码的跨学科技术解析

1. 项目概述&#xff1a;DNA作为终极数据存储介质的潜力在数据爆炸的时代&#xff0c;我们每天都在创造海量的数字信息&#xff0c;从家庭照片到科研数据&#xff0c;从商业档案到文化作品。传统的存储介质&#xff0c;无论是硬盘、磁带还是闪存&#xff0c;都面临着物理极限、…

作者头像 李华
网站建设 2026/5/14 7:52:04

外出采访的记者选工具,需警惕语音识别不支持离线问题

2026年需要外出做用户调研、访谈采访的产品技术同学&#xff0c;选语音转写工具首先要警惕不支持可用离线识别的坑。很多人遇到过弱网无网场景下转写中断&#xff0c;只存了原始音频&#xff0c;回去整理全靠人工&#xff0c;根本交不出管理层要的可量化提效方案。互联网To B产…

作者头像 李华
网站建设 2026/5/14 7:48:44

AI系统行为治理:构建确定性护栏与运行时安全控制

1. 项目概述&#xff1a;为AI系统构建确定性的行为护栏如果你正在构建一个会“动手”的AI应用——无论是能帮你写代码的智能助手&#xff0c;还是能操作数据库的自动化流程&#xff0c;甚至是部署在物理设备上的机器人——那么你迟早会面临一个核心问题&#xff1a;如何确保它只…

作者头像 李华
网站建设 2026/5/14 7:45:26

飞书文档批量导出工具:3步完成500+文档自动化迁移

飞书文档批量导出工具&#xff1a;3步完成500文档自动化迁移 【免费下载链接】feishu-doc-export 飞书文档导出服务 项目地址: https://gitcode.com/gh_mirrors/fe/feishu-doc-export 还在为飞书文档迁移而烦恼吗&#xff1f;手动下载几百个文档不仅耗时耗力&#xff0c…

作者头像 李华