javascript
测试与调试
By AI-Writer 10 min read
调试和测试是软件开发的日常。本章覆盖浏览器调试工具的核心用法、单元测试框架的选择与使用,以及如何通过测试驱动开发(TDD)提升代码质量。
Chrome DevTools 调试技巧
断点类型
javascript
function calculateTotal(items) {
let total = 0;
for (const item of items) {
// 1. 行断点:点击行号
total += item.price * item.quantity;
// 2. 条件断点:右键行号 → Add conditional breakpoint
// 条件: item.price > 100
// 3. 日志断点:右键行号 → Add logpoint
// 日志: item.name = {item.name}, subtotal = {total}
}
// 4. debugger 语句(代码中硬编码断点)
debugger;
return total;
}Watch 表达式与调用栈
调试面板中的实用功能:
- Watch:监视变量或表达式的实时值,如
items.length * 2 - Call Stack:查看当前执行的调用链,点击帧可查看该层的局部变量
- Scope:查看当前作用域内的所有变量(Local、Closure、Global)
- Breakpoints 面板:统一管理所有断点,可批量启用/禁用
Console 高级用法
javascript
const users = [
{ name: 'Alice', age: 28 },
{ name: 'Bob', age: 35 },
];
// 表格形式展示数据
console.table(users);
// 分组日志
console.group('Processing users');
users.forEach(u => console.log(u.name));
console.groupEnd();
// 断言:条件为 false 时输出错误
console.assert(users.length > 0, 'Users array is empty');
// 性能计时
console.time('sort');
users.sort((a, b) => a.age - b.age);
console.timeEnd('sort'); // sort: 0.123ms
// 堆栈追踪
console.trace('Current call stack');网络请求调试
Network 面板中:
- Preserve log:刷新页面后保留之前的请求记录
- Filter:按类型过滤(XHR/Fetch、JS、CSS、Img)
- Initiator 列:查看请求是由哪行代码发起的
- Copy as fetch/cURL:快速复制请求用于复现
断言与异常处理
防御式编程
javascript
function divide(a, b) {
// 运行时断言(生产环境可能被移除)
console.assert(typeof a === 'number' && typeof b === 'number',
'Arguments must be numbers');
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
// 使用 try...catch 处理异常
try {
const result = divide(10, 0);
} catch (error) {
console.error('Calculation failed:', error.message);
} finally {
// 无论是否异常都会执行
console.log('Cleanup resources');
}自定义错误类型
javascript
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
class NetworkError extends Error {
constructor(status, message) {
super(message);
this.name = 'NetworkError';
this.status = status;
}
}
// 根据错误类型做不同处理
function handleError(error) {
if (error instanceof ValidationError) {
return { type: 'validation', field: error.field, message: error.message };
}
if (error instanceof NetworkError) {
return { type: 'network', status: error.status, message: error.message };
}
return { type: 'unknown', message: error.message };
}单元测试框架
Vitest 入门
Vitest 是面向 Vite 生态的现代测试框架,语法与 Jest 兼容但启动更快:
javascript
// math.js
export function add(a, b) {
return a + b;
}
export function factorial(n) {
if (n < 0) throw new Error('Negative input');
if (n === 0 || n === 1) return 1;
return n * factorial(n - 1);
}
// math.test.js
import { describe, it, expect } from 'vitest';
import { add, factorial } from './math.js';
describe('add', () => {
it('should add two positive numbers', () => {
expect(add(2, 3)).toBe(5);
});
it('should handle negative numbers', () => {
expect(add(-1, 1)).toBe(0);
});
});
describe('factorial', () => {
it('should return 1 for 0 and 1', () => {
expect(factorial(0)).toBe(1);
expect(factorial(1)).toBe(1);
});
it('should calculate factorial correctly', () => {
expect(factorial(5)).toBe(120);
});
it('should throw on negative input', () => {
expect(() => factorial(-1)).toThrow('Negative input');
});
});常用断言方法
javascript
// 值比较
expect(value).toBe(expected); // 严格相等 ===
expect(value).toEqual(expected); // 深度相等(对象/数组)
expect(value).toStrictEqual(expected); // 深度相等 + 类型一致
// 真值判断
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
// 数字
expect(value).toBeGreaterThan(5);
expect(value).toBeCloseTo(0.3, 1); // 浮点数比较
// 字符串
expect(str).toContain('substring');
expect(str).toMatch(/regex/);
// 数组
expect(arr).toContain(item);
expect(arr).toHaveLength(3);
// 函数/异常
expect(fn).toThrow();
expect(fn).toThrow('expected message');
// 异步
await expect(fetchData()).resolves.toEqual({ id: 1 });
await expect(fetchData()).rejects.toThrow('Network error');Mock 函数
javascript
import { vi, describe, it, expect } from 'vitest';
import { processOrder } from './order.js';
describe('processOrder', () => {
it('should call callback with correct arguments', () => {
const callback = vi.fn();
processOrder({ id: 1, amount: 100 }, callback);
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith(1, 100);
});
it('should mock module', async () => {
// 模拟整个模块
vi.mock('./api.js', () => ({
fetchUser: vi.fn().mockResolvedValue({ name: 'Mock User' }),
}));
const { fetchUser } = await import('./api.js');
const user = await fetchUser(1);
expect(user.name).toBe('Mock User');
});
});测试异步代码
javascript
describe('async operations', () => {
it('should resolve with data', async () => {
const data = await fetchUser(1);
expect(data.id).toBe(1);
});
it('should reject on error', async () => {
await expect(fetchUser(-1)).rejects.toThrow('Invalid ID');
});
// 使用 fake timers 控制定时器
it('should debounce correctly', () => {
vi.useFakeTimers();
const debouncedFn = debounce(callback, 300);
debouncedFn();
debouncedFn();
debouncedFn();
expect(callback).not.toHaveBeenCalled();
vi.advanceTimersByTime(300);
expect(callback).toHaveBeenCalledTimes(1);
vi.useRealTimers();
});
});测试覆盖率
Vitest 内置覆盖率收集:
bash
# 运行测试并生成覆盖率报告
npx vitest run --coverage覆盖率指标说明:
| 指标 | 含义 | 目标 |
|---|---|---|
| Statements | 语句覆盖 | > 80% |
| Branches | 分支覆盖(if/else, switch) | > 70% |
| Functions | 函数覆盖 | > 80% |
| Lines | 行覆盖 | > 80% |
100% 覆盖率不等于没有 Bug,它只表示测试执行了所有代码路径。质量比数量更重要。
测试驱动开发(TDD)
TDD 的核心循环:红 → 绿 → 重构
实战示例:实现一个评分函数
javascript
// 第 1 步:先写测试(红)
describe('calculateRating', () => {
it('should return 5 for score >= 90', () => {
expect(calculateRating(95)).toBe(5);
});
it('should return 4 for score >= 80', () => {
expect(calculateRating(85)).toBe(4);
});
it('should return 1 for score < 60', () => {
expect(calculateRating(50)).toBe(1);
});
});
// 第 2 步:实现最小代码(绿)
export function calculateRating(score) {
if (score >= 90) return 5;
if (score >= 80) return 4;
if (score >= 70) return 3;
if (score >= 60) return 2;
return 1;
}
// 第 3 步:重构(保持测试通过)
// 可能将边界值提取为常量,或改用查表法测试策略金字塔
plaintext
/\
/ \ E2E 测试(少量)— 模拟真实用户操作
/____\
/ \ 集成测试(中等)— 模块间交互
/________\
\ 单元测试(大量)— 函数/组件独立测试- 单元测试:快速、独立、定位精确
- 集成测试:验证模块协作
- E2E 测试:覆盖关键用户路径
小结
- DevTools:断点、Watch、Call Stack、Console 表格/分组/计时
- 断言:
console.assert用于开发时检查,throw用于运行时错误 - Vitest:
describe/it/expect,支持 Mock 和 Fake Timers - 覆盖率:Statements / Branches / Functions / Lines 四个维度
- TDD:先写测试 → 最小实现 → 重构,形成质量内建的开发节奏
测试不是开发后的额外工作,而是设计更好 API 的安全网。当测试难以编写时,往往意味着代码设计需要改进。
#javascript
#testing
#debugging
#vitest
#jest
#devtools
评论
A
Written by
AI-Writer