Deno 的测试框架提供了强大而简洁的测试功能,使得编写和运行测试变得简单高效。了解 Deno 的测试系统对于保证代码质量至关重要。
测试框架概述
Deno 内置了测试框架,无需安装额外的测试库。测试文件通常以 _test.ts 或 .test.ts 结尾。
基本测试
1. 编写第一个测试
typescript// math_test.ts import { assertEquals } from "https://deno.land/std@0.208.0/testing/asserts.ts"; function add(a: number, b: number): number { return a + b; } Deno.test("add function adds two numbers", () => { assertEquals(add(1, 2), 3); assertEquals(add(-1, 1), 0); assertEquals(add(0, 0), 0); });
运行测试:
bashdeno test math_test.ts
2. 测试异步代码
typescript// async_test.ts import { assertEquals } from "https://deno.land/std@0.208.0/testing/asserts.ts"; async function fetchData(id: number): Promise<{ id: number; name: string }> { // 模拟异步操作 await new Promise(resolve => setTimeout(resolve, 100)); return { id, name: `Item ${id}` }; } Deno.test("fetchData returns correct data", async () => { const result = await fetchData(1); assertEquals(result.id, 1); assertEquals(result.name, "Item 1"); });
断言函数
1. 常用断言
typescriptimport { assertEquals, assertNotEquals, assertExists, assertStrictEquals, assertStringIncludes, assertArrayIncludes, assertMatch, assertThrows, assertRejects, } from "https://deno.land/std@0.208.0/testing/asserts.ts"; Deno.test("assertEquals examples", () => { assertEquals(1 + 1, 2); assertEquals("hello", "hello"); assertEquals({ a: 1 }, { a: 1 }); }); Deno.test("assertNotEquals examples", () => { assertNotEquals(1, 2); assertNotEquals("hello", "world"); }); Deno.test("assertExists examples", () => { assertExists("hello"); assertExists(42); assertExists({ key: "value" }); }); Deno.test("assertStrictEquals examples", () => { assertStrictEquals(1, 1); assertStrictEquals("hello", "hello"); // assertStrictEquals({ a: 1 }, { a: 1 }); // 会失败,因为引用不同 }); Deno.test("assertStringIncludes examples", () => { assertStringIncludes("hello world", "world"); assertStringIncludes("Deno is awesome", "Deno"); }); Deno.test("assertArrayIncludes examples", () => { assertArrayIncludes([1, 2, 3], [2]); assertArrayIncludes(["a", "b", "c"], ["b", "c"]); }); Deno.test("assertMatch examples", () => { assertMatch("hello@deno.com", /@/); assertMatch("12345", /^\d+$/); });
2. 测试异常
typescriptimport { assertThrows, assertRejects } from "https://deno.land/std@0.208.0/testing/asserts.ts"; function divide(a: number, b: number): number { if (b === 0) { throw new Error("Division by zero"); } return a / b; } Deno.test("divide throws error on zero", () => { assertThrows( () => divide(10, 0), Error, "Division by zero" ); }); async function asyncDivide(a: number, b: number): Promise<number> { if (b === 0) { throw new Error("Division by zero"); } return a / b; } Deno.test("asyncDivide rejects on zero", async () => { await assertRejects( () => asyncDivide(10, 0), Error, "Division by zero" ); });
测试配置
1. 测试选项
typescriptDeno.test({ name: "test with options", fn: () => { // 测试代码 }, permissions: { read: true, net: true, }, sanitizeOps: true, sanitizeResources: true, sanitizeExit: true, });
2. 超时设置
typescriptDeno.test({ name: "slow test with timeout", fn: async () => { await new Promise(resolve => setTimeout(resolve, 2000)); }, timeout: 5000, // 5秒超时 });
3. 忽略测试
typescriptDeno.test({ name: "ignored test", ignore: true, fn: () => { // 这个测试会被跳过 }, }); // 或者使用 only 只运行特定测试 Deno.test({ name: "only this test", only: true, fn: () => { // 只运行这个测试 }, });
测试组织
1. 测试套件
typescript// user_test.ts import { assertEquals } from "https://deno.land/std@0.208.0/testing/asserts.ts"; class User { constructor( public id: number, public name: string, public email: string ) {} greet(): string { return `Hello, ${this.name}!`; } isValid(): boolean { return this.id > 0 && this.name.length > 0 && this.email.includes("@"); } } Deno.test("User class - constructor", () => { const user = new User(1, "John", "john@example.com"); assertEquals(user.id, 1); assertEquals(user.name, "John"); assertEquals(user.email, "john@example.com"); }); Deno.test("User class - greet", () => { const user = new User(1, "John", "john@example.com"); assertEquals(user.greet(), "Hello, John!"); }); Deno.test("User class - isValid", () => { const validUser = new User(1, "John", "john@example.com"); assertEquals(validUser.isValid(), true); const invalidUser = new User(0, "", "invalid"); assertEquals(invalidUser.isValid(), false); });
2. 测试钩子
typescriptDeno.test({ name: "test with setup and teardown", async fn() { // Setup const db = await connectDatabase(); try { // Test const user = await db.createUser({ name: "John" }); assertEquals(user.name, "John"); } finally { // Teardown await db.close(); } }, sanitizeOps: false, sanitizeResources: false, });
Mock 和 Stub
1. 简单的 Mock
typescript// api_test.ts import { assertEquals } from "https://deno.land/std@0.208.0/testing/asserts.ts"; interface APIClient { fetchData(id: number): Promise<any>; } class RealAPIClient implements APIClient { async fetchData(id: number): Promise<any> { const response = await fetch(`https://api.example.com/data/${id}`); return response.json(); } } class MockAPIClient implements APIClient { private data: Map<number, any> = new Map(); setData(id: number, data: any) { this.data.set(id, data); } async fetchData(id: number): Promise<any> { return this.data.get(id); } } Deno.test("MockAPIClient returns mock data", async () => { const mockClient = new MockAPIClient(); mockClient.setData(1, { id: 1, name: "Test" }); const result = await mockClient.fetchData(1); assertEquals(result.id, 1); assertEquals(result.name, "Test"); });
2. 使用 Spy
typescript// spy_test.ts import { assertEquals } from "https://deno.land/std@0.208.0/testing/asserts.ts"; class Spy<T extends (...args: any[]) => any> { calls: Array<{ args: Parameters<T>; result: ReturnType<T> }> = []; wrap(fn: T): T { return ((...args: Parameters<T>) => { const result = fn(...args); this.calls.push({ args, result }); return result; }) as T; } callCount(): number { return this.calls.length; } calledWith(...args: Parameters<T>): boolean { return this.calls.some(call => JSON.stringify(call.args) === JSON.stringify(args) ); } } function processData(data: string, callback: (result: string) => void) { const result = data.toUpperCase(); callback(result); } Deno.test("processData calls callback", () => { const spy = new Spy<(result: string) => void>(); const callbackSpy = spy.wrap((result: string) => { console.log(result); }); processData("hello", callbackSpy); assertEquals(spy.callCount(), 1); assertEquals(spy.calledWith("HELLO"), true); });
集成测试
1. HTTP 服务器测试
typescript// server_test.ts import { assertEquals } from "https://deno.land/std@0.208.0/testing/asserts.ts"; import { serve } from "https://deno.land/std@0.208.0/http/server.ts"; async function startTestServer(): Promise<{ port: number; stop: () => Promise<void> }> { const handler = async (req: Request): Promise<Response> => { const url = new URL(req.url); if (url.pathname === "/") { return new Response("Hello, World!"); } if (url.pathname === "/api/users") { return new Response(JSON.stringify([{ id: 1, name: "John" }]), { headers: { "Content-Type": "application/json" }, }); } return new Response("Not Found", { status: 404 }); }; const server = await serve(handler, { port: 0 }); // 使用随机端口 const port = (server.addr as Deno.NetAddr).port; return { port, stop: async () => { server.shutdown(); }, }; } Deno.test("HTTP server responds to root", async () => { const { port, stop } = await startTestServer(); try { const response = await fetch(`http://localhost:${port}/`); assertEquals(response.status, 200); assertEquals(await response.text(), "Hello, World!"); } finally { await stop(); } }); Deno.test("HTTP server returns JSON for /api/users", async () => { const { port, stop } = await startTestServer(); try { const response = await fetch(`http://localhost:${port}/api/users`); assertEquals(response.status, 200); assertEquals(response.headers.get("Content-Type"), "application/json"); const data = await response.json(); assertEquals(Array.isArray(data), true); assertEquals(data[0].name, "John"); } finally { await stop(); } });
2. 文件系统测试
typescript// file_test.ts import { assertEquals, assertExists } from "https://deno.land/std@0.208.0/testing/asserts.ts"; async function createTestDirectory(): Promise<string> { const testDir = await Deno.makeTempDir(); return testDir; } Deno.test("file operations", async () => { const testDir = await createTestDirectory(); try { const filePath = `${testDir}/test.txt`; const content = "Hello, Deno!"; // 写入文件 await Deno.writeTextFile(filePath, content); // 读取文件 const readContent = await Deno.readTextFile(filePath); assertEquals(readContent, content); // 检查文件存在 const stat = await Deno.stat(filePath); assertExists(stat); assertEquals(stat.isFile, true); } finally { // 清理测试目录 await Deno.remove(testDir, { recursive: true }); } });
测试覆盖率
1. 生成覆盖率报告
bash# 运行测试并生成覆盖率 deno test --coverage=coverage # 生成覆盖率报告 deno coverage coverage --lcov --output=coverage.lcov # 在浏览器中查看覆盖率 deno coverage coverage --html
2. 覆盖率配置
json// deno.json { "compilerOptions": { "strict": true }, "test": { "include": ["src/**/*_test.ts", "tests/**/*.ts"], "exclude": ["node_modules/"] } }
测试最佳实践
1. 测试命名
typescript// 好的测试名称 Deno.test("add returns sum of two numbers", () => {}); Deno.test("divide throws error when dividing by zero", () => {}); Deno.test("fetchData returns user object when given valid ID", () => {}); // 不好的测试名称 Deno.test("test add", () => {}); Deno.test("it works", () => {});
2. AAA 模式(Arrange-Act-Assert)
typescriptDeno.test("calculateTotal returns correct total", () => { // Arrange - 准备测试数据 const items = [ { price: 10, quantity: 2 }, { price: 5, quantity: 3 }, ]; const expectedTotal = 35; // Act - 执行被测试的操作 const total = calculateTotal(items); // Assert - 验证结果 assertEquals(total, expectedTotal); });
3. 测试隔离
typescriptDeno.test("user creation", async () => { // 每个测试使用独立的数据库连接 const db = await createTestDatabase(); try { const user = await db.createUser({ name: "John" }); assertEquals(user.name, "John"); } finally { // 清理资源 await db.close(); } });
4. 测试数据构建器
typescript// test-builder.ts class UserBuilder { private user: Partial<User> = { id: 1, name: "John Doe", email: "john@example.com", }; withId(id: number): UserBuilder { this.user.id = id; return this; } withName(name: string): UserBuilder { this.user.name = name; return this; } withEmail(email: string): UserBuilder { this.user.email = email; return this; } build(): User { return this.user as User; } } // 使用构建器创建测试数据 Deno.test("user validation", () => { const user = new UserBuilder() .withId(1) .withName("John") .withEmail("john@example.com") .build(); assertEquals(user.isValid(), true); });
运行测试
1. 基本命令
bash# 运行所有测试 deno test # 运行特定测试文件 deno test math_test.ts # 监听模式(文件变化时自动运行) deno test --watch # 并行运行测试 deno test --parallel # 显示详细输出 deno test --verbose # 只运行失败的测试 deno test --fail-fast # 允许所有权限 deno test --allow-all
2. 过滤测试
bash# 运行匹配模式的测试 deno test --filter="user" # 运行特定测试 deno test --filter="add function"
最佳实践总结
- 测试独立性:每个测试应该独立运行,不依赖其他测试
- 清晰的命名:测试名称应该清楚地描述测试的内容
- AAA 模式:使用 Arrange-Act-Assert 模式组织测试代码
- 适当的断言:使用最合适的断言函数
- 测试边界情况:测试正常情况和边界情况
- 保持简单:测试应该简单、快速、易于理解
- 定期运行:在 CI/CD 中定期运行测试
- 覆盖率监控:监控测试覆盖率,确保代码质量
Deno 的测试框架提供了强大而简洁的功能,通过合理使用这些功能,可以构建高质量的测试套件,确保代码的可靠性和可维护性。