Deno's testing framework provides powerful and concise testing capabilities, making writing and running tests simple and efficient. Understanding Deno's testing system is crucial for ensuring code quality.
Testing Framework Overview
Deno includes a built-in testing framework, no need to install additional testing libraries. Test files typically end with _test.ts or .test.ts.
Basic Testing
1. Writing Your First Test
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); });
Run tests:
bashdeno test math_test.ts
2. Testing Async Code
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 }> { // Simulate async operation 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"); });
Assertion Functions
1. Common Assertions
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 }); // Will fail because references are different }); 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. Testing Exceptions
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" ); });
Test Configuration
1. Test Options
typescriptDeno.test({ name: "test with options", fn: () => { // Test code }, permissions: { read: true, net: true, }, sanitizeOps: true, sanitizeResources: true, sanitizeExit: true, });
2. Timeout Settings
typescriptDeno.test({ name: "slow test with timeout", fn: async () => { await new Promise(resolve => setTimeout(resolve, 2000)); }, timeout: 5000, // 5 second timeout });
3. Ignoring Tests
typescriptDeno.test({ name: "ignored test", ignore: true, fn: () => { // This test will be skipped }, }); // Or use only to run specific tests Deno.test({ name: "only this test", only: true, fn: () => { // Only run this test }, });
Test Organization
1. Test Suites
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. Test Hooks
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 and Stub
1. Simple 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. Using 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); });
Integration Testing
1. HTTP Server Testing
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 }); // Use random port 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 System Testing
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!"; // Write file await Deno.writeTextFile(filePath, content); // Read file const readContent = await Deno.readTextFile(filePath); assertEquals(readContent, content); // Check file exists const stat = await Deno.stat(filePath); assertExists(stat); assertEquals(stat.isFile, true); } finally { // Clean up test directory await Deno.remove(testDir, { recursive: true }); } });
Test Coverage
1. Generate Coverage Report
bash# Run tests and generate coverage deno test --coverage=coverage # Generate coverage report deno coverage coverage --lcov --output=coverage.lcov # View coverage in browser deno coverage coverage --html
2. Coverage Configuration
json// deno.json { "compilerOptions": { "strict": true }, "test": { "include": ["src/**/*_test.ts", "tests/**/*.ts"], "exclude": ["node_modules/"] } }
Testing Best Practices
1. Test Naming
typescript// Good test names 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", () => {}); // Bad test names Deno.test("test add", () => {}); Deno.test("it works", () => {});
2. AAA Pattern (Arrange-Act-Assert)
typescriptDeno.test("calculateTotal returns correct total", () => { // Arrange - Prepare test data const items = [ { price: 10, quantity: 2 }, { price: 5, quantity: 3 }, ]; const expectedTotal = 35; // Act - Execute the operation being tested const total = calculateTotal(items); // Assert - Verify the result assertEquals(total, expectedTotal); });
3. Test Isolation
typescriptDeno.test("user creation", async () => { // Each test uses independent database connection const db = await createTestDatabase(); try { const user = await db.createUser({ name: "John" }); assertEquals(user.name, "John"); } finally { // Clean up resources await db.close(); } });
4. Test Data Builder
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; } } // Use builder to create test data Deno.test("user validation", () => { const user = new UserBuilder() .withId(1) .withName("John") .withEmail("john@example.com") .build(); assertEquals(user.isValid(), true); });
Running Tests
1. Basic Commands
bash# Run all tests deno test # Run specific test file deno test math_test.ts # Watch mode (auto-run on file changes) deno test --watch # Run tests in parallel deno test --parallel # Show verbose output deno test --verbose # Only run failed tests deno test --fail-fast # Allow all permissions deno test --allow-all
2. Filtering Tests
bash# Run tests matching pattern deno test --filter="user" # Run specific test deno test --filter="add function"
Best Practices Summary
- Test independence: Each test should run independently without depending on other tests
- Clear naming: Test names should clearly describe what is being tested
- AAA pattern: Use Arrange-Act-Assert pattern to organize test code
- Appropriate assertions: Use the most appropriate assertion functions
- Test edge cases: Test both normal and edge cases
- Keep it simple: Tests should be simple, fast, and easy to understand
- Run regularly: Run tests regularly in CI/CD
- Coverage monitoring: Monitor test coverage to ensure code quality
Deno's testing framework provides powerful and concise features. By using these features appropriately, you can build high-quality test suites that ensure code reliability and maintainability.