Deno 入门教程 / 第 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/bdd | describe/it 风格 |
| Mock | @std/testing/mock | 函数/方法 Mock |
| 时间模拟 | @std/testing/time | FakeTime |
| 快照 | assertSnapshot | 快照测试 |
| 基准测试 | deno bench | 性能测试 |
| 覆盖率 | --coverage | 覆盖率报告 |
📖 扩展阅读
下一章:第 13 章:Fresh 框架 → 学习使用 Fresh 构建全栈 Web 应用。