本页内容
编写测试
在软件开发中,测试至关重要,它能确保你的代码按预期工作,并在你做出更改后继续正常运行。测试验证你的函数、模块和应用程序行为正确,能够妥善处理边缘情况,并保持预期的性能特征。
为什么测试很重要 跳转到标题
测试你的代码可以帮助你在代码发布到生产环境之前捕获错误、问题或回归,从而节省时间和资源。测试还有助于规划应用程序的逻辑,它们可以作为代码预期用途的人类可读描述。
Deno 提供了内置的测试能力,使得在你的项目中实现健壮的测试实践变得简单直接。
使用 Deno.test
编写测试 跳转到标题
在 Deno 中定义测试非常简单——使用 Deno.test()
函数将你的测试注册到测试运行器。此函数接受测试名称和函数,或包含更详细选项的配置对象。所有符合 *_test.{ts,js,mjs,jsx,tsx}
或 *.test.{ts,js,mjs,jsx,tsx}
等模式的文件中的测试函数,在你运行 deno test
命令时都会被自动发现和执行。
以下是定义测试的基本方式
// Basic test with a name and function
Deno.test("my first test", () => {
// Your test code here
});
// Test with configuration options
Deno.test({
name: "my configured test",
fn: () => {
// Your test code here
},
ignore: false, // Optional: set to true to skip this test
only: false, // Optional: set to true to only run this test
permissions: { // Optional: specify required permissions
read: true,
write: false,
},
});
一个简单的测试示例 跳转到标题
让我们从一个简单的测试开始。创建一个名为 main_test.ts
的文件,我们将在其中使用 Deno 的测试 API 和来自 Deno 标准库的 assertEquals
函数来测试一个基本的加法操作。
我们使用 Deno.test
并提供一个描述测试功能的名称
// hello_test.ts
import { assertEquals } from "jsr:@std/assert";
// Function we want to test
function add(a: number, b: number): number {
return a + b;
}
Deno.test("basic addition test", () => {
// Arrange - set up the test data
const a = 1;
const b = 2;
// Act - call the function being tested
const result = add(a, b);
// Assert - verify the result is what we expect
assertEquals(result, 3);
});
要运行此测试,请使用 deno test
命令
deno test hello_test.ts
你应该会看到表明测试已通过的输出
running 1 test from ./hello_test.ts
basic addition test ... ok (2ms)
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (2ms)
尝试更改函数实现以使测试失败
function add(a: number, b: number): number {
return a - b; // Changed from addition to subtraction
}
你会看到一个清晰显示问题所在的错误消息
running 1 test from ./hello_test.ts
basic addition test ... FAILED (3ms)
failures:
basic addition test => ./hello_test.ts:12:3
error: AssertionError: Values are not equal:
[Diff] Actual / Expected
- -1
+ 3
at assertEquals (https://jsr.deno.org.cn/@std/assert@0.218.2/assert_equals.ts:31:9)
at Object.fn (file:///path/to/hello_test.ts:12:3)
at asyncOpSanitizer (ext:core/01_core.js:199:13)
at Object.sanitizeOps (ext:core/01_core.js:219:15)
at runTest (ext:test/06_test_runner.js:319:29)
at test (ext:test/06_test_runner.js:593:7)
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out (3ms)
这种清晰的反馈有助于你快速识别并修复代码中的问题。
测试结构和组织 跳转到标题
Deno 会自动查找并运行符合 *_test.{ts,js,mjs,jsx,tsx}
或 *.test.{ts,js,mjs,jsx,tsx}
等命名模式的测试。组织测试文件的方法有很多,我们建议将单元测试与它们所测试的代码放在一起,并将集成测试和配置保存在一个 tests
目录中。这使得单元测试能够立即被发现并简化了导入,同时保持了不同类型测试之间的分离。
以下是项目与测试结合的结构示例
my-deno-project/
├── src/
│ ├── models/
│ │ ├── user.ts
│ │ ├── user_test.ts // Unit tests for user model
│ │ ├── product.ts
│ │ └── product_test.ts // Unit tests for product model
│ ├── services/
│ │ ├── auth-service.ts
│ │ ├── auth-service_test.ts // Unit tests for auth service
│ │ ├── data-service.ts
│ │ └── data-service_test.ts // Unit tests for data service
│ └── utils/
│ ├── helpers.ts
│ └── helpers_test.ts // Unit tests for helpers
├── tests/
│ ├── integration/ // Integration tests directory
│ │ ├── api_test.ts // Tests API endpoints
│ │ └── db_test.ts // Tests database interactions
│ ├── e2e/ // End-to-end tests
│ │ └── user_flow_test.ts // Tests complete user workflows
│ └── fixtures/ // Shared test data and utilities
│ ├── test_data.ts // Test data used across tests
│ └── setup.ts // Common setup functions
├── main.ts
└── deno.json // Project configuration
这种结构为测试配置提供了一个集中位置,同时保持了单元测试与其相关文件共存的优势。采用这种结构,你可以
# Run all tests
deno test
# Run only unit tests
deno test src/
# Run only integration tests
deno test tests/integration/
# Run specific module tests
deno test src/models/
# Run a specific test file
deno test src/models/user_test.ts
断言 跳转到标题
断言是有效测试的基石,它允许你验证代码是否按预期运行。它们检查特定条件是否为真,如果不是,则抛出错误,导致测试失败。良好的断言清晰、具体,并有助于在测试失败时准确识别问题所在。
Deno 的核心库中不包含断言,但你可以从 Deno 标准库中导入它们。
import {
assertArrayIncludes, // Check that array contains value
assertEquals, // Check that values are equal
assertExists, // Check that value is not null or undefined
assertMatch, // Check that string matches regex pattern
assertNotEquals, // Check that values are not equal
assertObjectMatch, // Check that object has expected properties
assertRejects, // Check that Promise rejects
assertStrictEquals, // Check that values are strictly equal (===)
assertStringIncludes, // Check that string contains substring
assertThrows, // Check that function throws an error
} from "jsr:@std/assert";
Deno.test("assertion examples", () => {
// Basic assertions
assertEquals(1 + 1, 2);
assertNotEquals("hello", "world");
assertExists("Hello");
// String assertions
assertStringIncludes("Hello, world!", "world");
assertMatch("deno@1.0.0", /^deno@\d+\.\d+\.\d+$/);
// Object assertions
assertObjectMatch(
{ name: "Jane", age: 25, city: "Tokyo" },
{ name: "Jane" }, // Only checks specified properties
);
// Strict equality (type + value)
assertStrictEquals("deno", "deno");
// Error assertions
assertThrows(
() => {
throw new Error("Something went wrong");
},
Error,
"Something went wrong",
);
});
对于喜欢流畅断言(Jest 用户所熟悉)的用户,可以使用 expect
模块
import { expect } from "jsr:@std/expect";
Deno.test("expect style assertions", () => {
// Basic matchers
expect(5).toBe(5);
expect({ name: "deno" }).toEqual({ name: "deno" });
// Collection matchers
expect([1, 2, 3]).toContain(2);
// Truthiness matchers
expect(true).toBeTruthy();
expect(0).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
// Number matchers
expect(100).toBeGreaterThan(99);
expect(1).toBeLessThan(2);
// String matchers
expect("Hello world").toMatch(/world/);
// Function/error matchers
expect(() => {
throw new Error("fail");
}).toThrow();
});
真实世界的示例 跳转到标题
这是一个测试处理用户数据函数的更真实的例子
// user_processor.ts
export function validateUser(user: any): { valid: boolean; errors: string[] } {
const errors: string[] = [];
if (!user.name || typeof user.name !== "string") {
errors.push("Name is required and must be a string");
}
if (!user.email || !user.email.includes("@")) {
errors.push("Valid email is required");
}
if (
user.age !== undefined && (typeof user.age !== "number" || user.age < 18)
) {
errors.push("Age must be a number and at least 18");
}
return {
valid: errors.length === 0,
errors,
};
}
// user_processor_test.ts
import { assertEquals } from "jsr:@std/assert";
import { validateUser } from "./user_processor.ts";
Deno.test("validateUser", async (t) => {
await t.step("should validate a correct user object", () => {
const user = {
name: "John Doe",
email: "john@example.com",
age: 30,
};
const result = validateUser(user);
assertEquals(result.valid, true);
assertEquals(result.errors.length, 0);
});
await t.step("should return errors for invalid user", () => {
const user = {
name: "",
email: "invalid-email",
age: 16,
};
const result = validateUser(user);
assertEquals(result.valid, false);
assertEquals(result.errors.length, 3);
assertEquals(result.errors[0], "Name is required and must be a string");
assertEquals(result.errors[1], "Valid email is required");
assertEquals(result.errors[2], "Age must be a number and at least 18");
});
await t.step("should handle missing properties", () => {
const user = {
name: "Jane Doe",
// email and age missing
};
const result = validateUser(user);
assertEquals(result.valid, false);
assertEquals(result.errors.length, 1);
assertEquals(result.errors[0], "Valid email is required");
});
});
异步测试 跳转到标题
Deno 自然地处理异步测试。只需将你的测试函数设为 async 并使用 await
import { assertEquals } from "jsr:@std/assert";
Deno.test("async test example", async () => {
const response = await fetch("https://deno.land");
const status = response.status;
assertEquals(status, 200);
});
测试异步函数 跳转到标题
测试返回 Promise 的函数时,你应始终等待结果。
// async-function.ts
export async function fetchUserData(userId: string) {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error(`Failed to fetch user: ${response.status}`);
}
return await response.json();
}
// async-function_test.ts
import { assertEquals, assertRejects } from "jsr:@std/assert";
import { fetchUserData } from "./async-function.ts";
Deno.test("fetchUserData success", async () => {
// Mock the fetch function for testing
globalThis.fetch = async (url: string) => {
const data = JSON.stringify({ id: "123", name: "Test User" });
return new Response(data, { status: 200 });
};
const userData = await fetchUserData("123");
assertEquals(userData.id, "123");
assertEquals(userData.name, "Test User");
});
Deno.test("fetchUserData failure", async () => {
// Mock the fetch function to simulate an error
globalThis.fetch = async (url: string) => {
return new Response("Not Found", { status: 404 });
};
await assertRejects(
async () => await fetchUserData("nonexistent"),
Error,
"Failed to fetch user: 404",
);
});
测试中的模拟 跳转到标题
模拟是将待测代码与其依赖项隔离的重要技术。Deno 提供了内置工具和第三方库来创建模拟。
基本模拟 跳转到标题
你可以通过用自己的实现替换函数或对象来创建简单的模拟。这使你能够控制依赖项的行为,并测试你的代码如何与它们交互。
// Example of a module with a function we want to mock
const api = {
fetchData: async () => {
const response = await fetch("https://api.example.com/data");
return response.json();
},
};
// In your test file
Deno.test("basic mocking example", async () => {
// Store the original function
const originalFetchData = api.fetchData;
// Replace with mock implementation
api.fetchData = async () => {
return { id: 1, name: "Test Data" };
};
try {
// Test using the mock
const result = await api.fetchData();
assertEquals(result, { id: 1, name: "Test Data" });
} finally {
// Restore the original function
api.fetchData = originalFetchData;
}
});
使用 Spy 函数 跳转到标题
Spies 允许你跟踪函数调用而不改变它们的行为
import { spy } from "jsr:@std/testing/mock";
Deno.test("spy example", () => {
// Create a spy on console.log
const consoleSpy = spy(console, "log");
// Call the function we're spying on
console.log("Hello");
console.log("World");
// Verify the function was called correctly
assertEquals(consoleSpy.calls.length, 2);
assertEquals(consoleSpy.calls[0].args, ["Hello"]);
assertEquals(consoleSpy.calls[1].args, ["World"]);
// Restore the original function
consoleSpy.restore();
});
有关更高级的模拟技术,请查阅我们的Deno 模拟专用指南。
覆盖率 跳转到标题
代码覆盖率是一种指标,可帮助你了解代码有多少部分正在被测试。它衡量在测试期间代码的哪些行、函数和分支被执行,让你深入了解可能缺乏适当测试的区域。
覆盖率分析有助于你
- 识别代码库中未经测试的部分
- 确保关键路径有测试
- 在进行更改时防止回归
- 衡量测试随时间推移的进展
高覆盖率并不能保证高质量的测试。它仅仅显示了哪些代码被执行了,而不是你的断言是否有意义或者边缘情况是否得到了正确处理。
Deno 提供了内置的覆盖率工具来帮助你分析测试覆盖率。要收集覆盖率信息
deno test --coverage=coverage_dir
这会在指定目录(此处为 coverage_dir
)中生成覆盖率数据。要查看人类可读的报告
deno coverage coverage_dir
你会看到类似以下的输出
file:///projects/my-project/src/utils.ts 85.7% (6/7)
file:///projects/my-project/src/models/user.ts 100.0% (15/15)
file:///projects/my-project/src/services/auth.ts 78.3% (18/23)
total: 87.5% (39/45)
为了获得更详细的洞察,你还可以生成 HTML 报告
deno coverage --html coverage_dir
这会在指定目录中创建一个交互式 HTML 报告,精确显示哪些行被覆盖以及哪些未被覆盖。
默认情况下,覆盖率工具会自动排除
- 测试文件(匹配
test.ts
或test.js
等模式) - 远程文件(那些不以
file:
开头的文件)
这确保你的覆盖率报告侧重于你的应用程序代码,而不是测试文件或外部依赖项。
覆盖率配置 跳转到标题
你可以使用 --exclude
标志从覆盖率报告中排除文件
deno coverage --exclude="test_,vendor/,_build/,node_modules/" coverage_dir
与 CI 集成 跳转到标题
对于持续集成环境,你可能希望强制执行最低覆盖率阈值
# In your GitHub Actions workflow
- name: Run tests with coverage
run: deno test --coverage=coverage_dir
- name: Check coverage meets threshold
run: |
COVERAGE=$(deno coverage coverage_dir | grep "total:" | grep -o '[0-9]\+\.[0-9]\+')
if (( $(echo "$COVERAGE < 80" | bc -l) )); then
echo "Test coverage is below 80%: $COVERAGE%"
exit 1
fi
在处理测试覆盖率时,请记住设定实际目标,追求有意义的覆盖率和高质量的测试,而不是盲目追求 100% 的覆盖率。
与其他测试框架的比较 跳转到标题
如果你来自其他 JavaScript 测试框架,以下是 Deno 测试能力与它们的比较
功能 | Deno | Jest | Mocha | Jasmine |
---|---|---|---|---|
设置 | 内置 | 需要安装 | 需要安装 | 需要安装 |
语法 | Deno.test() |
test() 、describe() |
it() 、describe() |
it() 、describe() |
断言 | 来自标准库 | 内置 expect | 需要断言库 | 内置 expect |
模拟 | 来自标准库 | 内置 jest.mock() | 需要 sinon 或类似库 | 内置 spies |
异步支持 | 原生 | 需要特殊处理 | 支持 Promise | 支持 Promise |
文件监听 | --watch 标志 |
监听模式 | 需要 nodemon | 需要额外工具 |
代码覆盖率 | 内置 | 内置 | 需要 istanbul | 需要 istanbul |
测试风格比较 跳转到标题
Deno
import { assertEquals } from "jsr:@std/assert";
Deno.test("add function", () => {
assertEquals(1 + 2, 3);
});
Jest
test("add function", () => {
expect(1 + 2).toBe(3);
});
Mocha
import { assert } from "chai";
describe("math", () => {
it("should add numbers", () => {
assert.equal(1 + 2, 3);
});
});
Jasmine
describe("math", () => {
it("should add numbers", () => {
expect(1 + 2).toBe(3);
});
});
后续步骤 跳转到标题
🦕 Deno 的内置测试能力使得编写和运行测试变得容易,而无需安装额外的测试框架或工具。通过遵循本教程中概述的模式和实践,你可以确保你的 Deno 应用程序经过充分测试且可靠。
有关 Deno 中测试的更多信息,请查阅