强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 12 章:测试

第 12 章:测试

12.1 Deno 测试概述

Deno 内置了完整的测试框架,无需安装 Jest、Mocha 等第三方工具:

功能Deno 内置命令
单元测试deno test
基准测试deno bench
快照测试内置支持
覆盖率deno test --coverage
Mock@std/testing/mock
模拟时间@std/testing/time

12.2 基本测试

编写第一个测试

// math_test.ts
import { assertEquals } from "jsr:@std/assert";

function add(a: number, b: number): number {
  return a + b;
}

Deno.test("加法测试", () => {
  assertEquals(add(1, 2), 3);
  assertEquals(add(-1, 1), 0);
  assertEquals(add(0, 0), 0);
});

运行测试:

deno test math_test.ts
# 输出:
# running 1 test from file:///path/to/math_test.ts
# test 加法测试 ... ok (2ms)
# 
# ok | 1 passed | 0 failed (10ms)

测试命名约定

// 文件名:xxx_test.ts 或 xxx.test.ts
// 函数名:描述性命名

Deno.test("两个正数相加应该返回正确结果", () => {
  assertEquals(add(2, 3), 5);
});

Deno.test("负数相加应该正确处理", () => {
  assertEquals(add(-2, -3), -5);
});

12.3 测试选项

基本选项

Deno.test({
  name: "带选项的测试",
  ignore: false,        // 设为 true 跳过此测试
  only: false,          // 设为 true 只运行此测试
  sanitizeOps: true,    // 检查是否有未关闭的异步操作
  sanitizeResources: true, // 检查是否有未关闭的资源
  permissions: {        // 设置此测试的权限
    read: true,
    net: ["api.example.com"],
  },
}, () => {
  // 测试代码
});

跳过测试

// 跳过单个测试
Deno.test("暂时跳过", { ignore: true }, () => {
  // 不会运行
});

// 只运行特定测试
Deno.test("只运行这个", { only: true }, () => {
  // 只有 only: true 的测试会运行
});

// 条件跳过
Deno.test("跳过 Windows", { ignore: Deno.build.os === "windows" }, () => {
  // 在 Windows 上跳过
});

12.4 异步测试

import { assertEquals, assertRejects } from "jsr:@std/assert";

// async/await 测试
Deno.test("异步操作测试", async () => {
  const response = await fetch("https://jsonplaceholder.typicode.com/todos/1");
  const data = await response.json();
  assertEquals(data.id, 1);
});

// 测试 Promise rejection
Deno.test("拒绝测试", async () => {
  await assertRejects(
    () => Promise.reject(new Error("失败")),
    Error,
    "失败"
  );
});

// 测试超时
Deno.test("超时测试", { sanitizeResources: false }, async () => {
  const controller = new AbortController();
  setTimeout(() => controller.abort(), 100);
  
  try {
    await fetch("https://api.example.com/slow", { signal: controller.signal });
  } catch (e) {
    assertEquals(e.name, "AbortError");
  }
});

12.5 断言库

Deno 提供了丰富的断言函数:

import {
  assertEquals,         // 严格相等
  assertNotEquals,      // 不相等
  assertStrictEquals,   // 引用相等 ===
  assertExists,         // 非 null/undefined
  assertThrows,         // 同步抛出异常
  assertRejects,        // 异步抛出异常
  assertStringIncludes, // 字符串包含
  assertArrayIncludes,  // 数组包含
  assertMatch,          // 正则匹配
  assertObjectMatch,    // 对象部分匹配
  assertAlmostEquals,   // 数值近似相等
  assertSnapshot,       // 快照
} from "jsr:@std/assert";

// 基本相等
assertEquals({ a: 1 }, { a: 1 });  // 深度比较
assertStrictEquals(1, 1);           // 引用比较

// 字符串
assertStringIncludes("Hello World", "Hello");

// 数组
assertArrayIncludes([1, 2, 3], [1, 2]);

// 正则
assertMatch("hello123", /\d+/);

// 对象部分匹配
assertObjectMatch(
  { name: "Alice", age: 30, email: "[email protected]" },
  { name: "Alice", age: 30 }
);

// 数值近似
assertAlmostEquals(3.14, Math.PI, 0.01);

自定义断言

import { assert } from "jsr:@std/assert";

function assertPositive(value: number, msg?: string) {
  assert(value > 0, msg ?? `期望正数,实际得到 ${value}`);
}

Deno.test("自定义断言", () => {
  assertPositive(5);    // ✅
  assertPositive(-1);   // ❌ 抛出错误
});

12.6 测试套件(describe/it)

import { describe, it } from "jsr:@std/testing/bdd";
import { assertEquals, assertThrows } from "jsr:@std/assert";

// 使用 describe 组织测试
describe("Calculator", () => {
  describe("add", () => {
    it("应该正确相加两个正数", () => {
      assertEquals(add(2, 3), 5);
    });

    it("应该正确处理负数", () => {
      assertEquals(add(-2, -3), -5);
    });
  });

  describe("divide", () => {
    it("应该正确相除", () => {
      assertEquals(divide(10, 2), 5);
    });

    it("除以零应该抛出错误", () => {
      assertThrows(() => divide(10, 0), Error, "除以零");
    });
  });
});

12.7 测试生命周期(Hooks)

import { describe, it, beforeEach, afterEach, beforeAll, afterAll } from "jsr:@std/testing/bdd";
import { assertEquals } from "jsr:@std/assert";

describe("数据库测试", () => {
  let db: Database;

  beforeAll(async () => {
    // 整个套件开始前执行一次
    db = await Database.connect("test.db");
    await db.migrate();
  });

  afterAll(async () => {
    // 整个套件结束后执行一次
    await db.close();
  });

  beforeEach(async () => {
    // 每个测试前执行
    await db.clearTables();
    await db.seed();
  });

  afterEach(() => {
    // 每个测试后执行
  });

  it("查询用户", async () => {
    const users = await db.query("SELECT * FROM users");
    assertEquals(users.length, 2);
  });

  it("创建用户", async () => {
    await db.insert("users", { name: "Charlie" });
    const users = await db.query("SELECT * FROM users");
    assertEquals(users.length, 3);
  });
});

12.8 Mock 与 Stub

基本 Mock

import { mock } from "jsr:@std/testing/mock";
import { assertEquals } from "jsr:@std/assert";

Deno.test("Mock 函数", () => {
  const fn = mock.fn();
  
  fn("a");
  fn("b");
  fn("c");
  
  // 检查调用次数
  assertEquals(fn.calls.length, 3);
  
  // 检查调用参数
  assertEquals(fn.calls[0].args, ["a"]);
  assertEquals(fn.calls[1].args, ["b"]);
  
  // 重置 mock
  fn.mock.calls = [];
  assertEquals(fn.calls.length, 0);
});

带返回值的 Mock

import { mock } from "jsr:@std/testing/mock";
import { assertEquals } from "jsr:@std/assert";

Deno.test("Mock 返回值", () => {
  const fn = mock.fn(() => 42);
  assertEquals(fn(), 42);
});

Deno.test("Mock 多次返回不同值", () => {
  const fn = mock.fn(
    () => 1,
    () => 2,
    () => 3
  );
  
  assertEquals(fn(), 1);
  assertEquals(fn(), 2);
  assertEquals(fn(), 3);
});

Stub 对象方法

import { stub } from "jsr:@std/testing/mock";
import { assertEquals } from "jsr:@std/assert";

class UserService {
  async getUser(id: number) {
    // 实际会调用数据库
    return { id, name: "Original" };
  }
}

Deno.test("Stub 方法", async () => {
  const service = new UserService();
  
  // 创建 stub
  const stubbed = stub(service, "getUser", () => 
    Promise.resolve({ id: 1, name: "Mocked" })
  );
  
  const user = await service.getUser(1);
  assertEquals(user.name, "Mocked");
  
  // 恢复原始方法
  stubbed.restore();
  
  const realUser = await service.getUser(1);
  assertEquals(realUser.name, "Original");
});

Mock fetch

import { mock } from "jsr:@std/testing/mock";
import { assertEquals } from "jsr:@std/assert";

Deno.test("Mock fetch", async () => {
  // 保存原始 fetch
  const originalFetch = globalThis.fetch;
  
  // Mock fetch
  globalThis.fetch = mock.fn(() =>
    Promise.resolve(new Response(JSON.stringify({ id: 1, name: "Test" }), {
      status: 200,
      headers: { "content-type": "application/json" },
    }))
  );
  
  const response = await fetch("https://api.example.com/users/1");
  const data = await response.json();
  assertEquals(data.name, "Test");
  
  // 恢复
  globalThis.fetch = originalFetch;
});

12.9 时间模拟

import { FakeTime } from "jsr:@std/testing/time";
import { assertEquals } from "jsr:@std/assert";

Deno.test("模拟时间", async () => {
  using time = new FakeTime();
  
  let count = 0;
  const interval = setInterval(() => {
    count++;
  }, 1000);
  
  // 时间前进 3 秒
  await time.tickAsync(3000);
  assertEquals(count, 3);
  
  clearInterval(interval);
});

12.10 快照测试

import { assertSnapshot } from "jsr:@std/testing/snapshot";

Deno.test("快照测试", async (t) => {
  const data = {
    name: "Alice",
    age: 30,
    items: [1, 2, 3],
  };
  
  await assertSnapshot(t, data);
});

Deno.test("渲染结果快照", async (t) => {
  const html = renderComponent("<Button>Click</Button>");
  await assertSnapshot(t, html);
});

运行并更新快照:

# 运行测试
deno test

# 更新快照
deno test -- --update

12.11 基准测试(Benchmark)

// bench_test.ts
Deno.bench("字符串拼接", () => {
  let s = "";
  for (let i = 0; i < 1000; i++) {
    s += "a";
  }
});

Deno.bench("数组 join", () => {
  const arr: string[] = [];
  for (let i = 0; i < 1000; i++) {
    arr.push("a");
  }
  arr.join("");
});

Deno.bench("Array.from", () => {
  Array.from({ length: 1000 }, () => "a").join("");
});

运行基准测试:

deno bench bench_test.ts
# 输出:
#   CPU | 10x |  3.425µs/iter |  34.25µs total
#   CPU | 10x |  8.125µs/iter |  81.25µs total
#   CPU | 10x |  5.625µs/iter |  56.25µs total

基准测试选项

Deno.bench({
  name: "复杂计算",
  baseline: true,   // 标记为基准线
  group: "算法对比",  // 分组
  n: 1000,          // 运行次数
  warmup: 100,      // 预热次数
  permissions: {    // 权限
    read: false,
  },
}, () => {
  // 复杂计算
  let sum = 0;
  for (let i = 0; i < 1000000; i++) {
    sum += Math.sqrt(i);
  }
});

12.12 测试覆盖率

# 运行测试并收集覆盖率
deno test --coverage

# 查看覆盖率报告
deno coverage

# 生成 HTML 报告
deno coverage --html

# 查看具体文件的覆盖率详情
deno coverage --lcov > coverage.lcov

# 排除特定文件
deno test --coverage --exclude='test/'

覆盖率配置

// deno.json
{
  "tasks": {
    "test": "deno test",
    "test:coverage": "deno test --coverage=coverage",
    "coverage:report": "deno coverage coverage --html"
  }
}

12.13 实战:完整的测试示例

// src/calculator.ts
export class Calculator {
  #history: string[] = [];

  add(a: number, b: number): number {
    const result = a + b;
    this.#history.push(`${a} + ${b} = ${result}`);
    return result;
  }

  divide(a: number, b: number): number {
    if (b === 0) throw new Error("除以零");
    const result = a / b;
    this.#history.push(`${a} / ${b} = ${result}`);
    return result;
  }

  getHistory(): readonly string[] {
    return this.#history;
  }
}
// src/calculator_test.ts
import { describe, it, beforeEach } from "jsr:@std/testing/bdd";
import { assertEquals, assertThrows, assertArrayIncludes } from "jsr:@std/assert";
import { Calculator } from "./calculator.ts";

describe("Calculator", () => {
  let calc: Calculator;

  beforeEach(() => {
    calc = new Calculator();
  });

  describe("add", () => {
    it("应该正确相加两个正数", () => {
      assertEquals(calc.add(2, 3), 5);
    });

    it("应该正确处理零", () => {
      assertEquals(calc.add(0, 0), 0);
    });

    it("应该正确处理负数", () => {
      assertEquals(calc.add(-5, 3), -2);
    });

    it("应该记录历史", () => {
      calc.add(1, 2);
      assertArrayIncludes(calc.getHistory(), ["1 + 2 = 3"]);
    });
  });

  describe("divide", () => {
    it("应该正确相除", () => {
      assertEquals(calc.divide(10, 2), 5);
    });

    it("除以零应该抛出错误", () => {
      assertThrows(
        () => calc.divide(10, 0),
        Error,
        "除以零"
      );
    });
  });
});
# 运行测试
deno test src/calculator_test.ts

# 运行所有测试
deno test

# 运行特定模式的测试
deno test --filter "Calculator"

12.14 本章小结

功能命令/模块说明
单元测试deno test内置测试框架
断言@std/assert丰富的断言函数
BDD@std/testing/bdddescribe/it 风格
Mock@std/testing/mock函数/方法 Mock
时间模拟@std/testing/timeFakeTime
快照assertSnapshot快照测试
基准测试deno bench性能测试
覆盖率--coverage覆盖率报告

📖 扩展阅读


下一章第 13 章:Fresh 框架 → 学习使用 Fresh 构建全栈 Web 应用。