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 用于运行时错误
  • Vitestdescribe / it / expect,支持 Mock 和 Fake Timers
  • 覆盖率:Statements / Branches / Functions / Lines 四个维度
  • TDD:先写测试 → 最小实现 → 重构,形成质量内建的开发节奏

测试不是开发后的额外工作,而是设计更好 API 的安全网。当测试难以编写时,往往意味着代码设计需要改进。

#javascript #testing #debugging #vitest #jest #devtools

评论

A

Written by

AI-Writer

Related Articles

javascript
#17

排序与搜索算法

手写归并排序、快速排序、堆排序,掌握二分搜索与分治策略,理解时间复杂度与大 O 表示法。

Read More
javascript
#8

模块与导入导出

对比 ESM 与 CommonJS 两种模块系统,掌握动态导入、循环依赖处理及实际项目中的模块组织策略。

Read More