Deno 的测试框架如何使用?
Deno 的测试框架提供了强大而简洁的测试功能,使得编写和运行测试变得简单高效。了解 Deno 的测试系统对于保证代码质量至关重要。测试框架概述Deno 内置了测试框架,无需安装额外的测试库。测试文件通常以 _test.ts 或 .test.ts 结尾。基本测试1. 编写第一个测试// math_test.tsimport { 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);});运行测试:deno test math_test.ts2. 测试异步代码// async_test.tsimport { 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. 常用断言import { 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. 测试异常import { 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. 测试选项Deno.test({ name: "test with options", fn: () => { // 测试代码 }, permissions: { read: true, net: true, }, sanitizeOps: true, sanitizeResources: true, sanitizeExit: true,});2. 超时设置Deno.test({ name: "slow test with timeout", fn: async () => { await new Promise(resolve => setTimeout(resolve, 2000)); }, timeout: 5000, // 5秒超时});3. 忽略测试Deno.test({ name: "ignored test", ignore: true, fn: () => { // 这个测试会被跳过 },});// 或者使用 only 只运行特定测试Deno.test({ name: "only this test", only: true, fn: () => { // 只运行这个测试 },});测试组织1. 测试套件// user_test.tsimport { 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. 测试钩子Deno.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 和 Stub1. 简单的 Mock// api_test.tsimport { 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// spy_test.tsimport { 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 服务器测试// server_test.tsimport { 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. 文件系统测试// file_test.tsimport { 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. 生成覆盖率报告# 运行测试并生成覆盖率deno test --coverage=coverage# 生成覆盖率报告deno coverage coverage --lcov --output=coverage.lcov# 在浏览器中查看覆盖率deno coverage coverage --html2. 覆盖率配置// deno.json{ "compilerOptions": { "strict": true }, "test": { "include": ["src/**/*_test.ts", "tests/**/*.ts"], "exclude": ["node_modules/"] }}测试最佳实践1. 测试命名// 好的测试名称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)Deno.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. 测试隔离Deno.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. 测试数据构建器// test-builder.tsclass 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. 基本命令# 运行所有测试deno test# 运行特定测试文件deno test math_test.ts# 监听模式(文件变化时自动运行)deno test --watch# 并行运行测试deno test --parallel# 显示详细输出deno test --verbose# 只运行失败的测试deno test --fail-fast# 允许所有权限deno test --allow-all2. 过滤测试# 运行匹配模式的测试deno test --filter="user"# 运行特定测试deno test --filter="add function"最佳实践总结测试独立性:每个测试应该独立运行,不依赖其他测试清晰的命名:测试名称应该清楚地描述测试的内容AAA 模式:使用 Arrange-Act-Assert 模式组织测试代码适当的断言:使用最合适的断言函数测试边界情况:测试正常情况和边界情况保持简单:测试应该简单、快速、易于理解定期运行:在 CI/CD 中定期运行测试覆盖率监控:监控测试覆盖率,确保代码质量Deno 的测试框架提供了强大而简洁的功能,通过合理使用这些功能,可以构建高质量的测试套件,确保代码的可靠性和可维护性。