测试中的打桩
打桩是一种强大的技术,它通过用受控的实现替换函数来隔离你正在测试的代码。虽然侦察(spies)在不改变行为的情况下监控函数调用,但桩更进一步,完全替换了原始实现,允许你在测试期间模拟特定的条件或行为。
什么是桩? 跳转到标题
桩是假实现,用于在测试期间替换真实函数。它们允许你
- 控制函数的返回值
- 模拟错误或特定边缘情况
- 防止调用数据库或API等外部服务
- 测试使用真实实现难以触发的代码路径
Deno 通过标准库的测试工具提供了强大的打桩能力。
基本桩用法 跳转到标题
这是一个演示如何对函数进行打桩的简单示例
import { assertEquals } from "jsr:@std/assert";
import { stub } from "jsr:@std/testing/mock";
// Original function
function getUserName(id: number): string {
// In a real app, this might call a database
return "Original User";
}
// Function under test
function greetUser(id: number): string {
const name = getUserName(id);
return `Hello, ${name}!`;
}
Deno.test("greetUser with stubbed getUserName", () => {
// Create a stub that returns a controlled value
const getUserNameStub = stub(globalThis, "getUserName", () => "Test User");
try {
// Test with the stubbed function
const greeting = greetUser(123);
assertEquals(greeting, "Hello, Test User!");
} finally {
// Always restore the original function
getUserNameStub.restore();
}
});
在此示例中,我们
- 从 Deno 的标准库导入必要的函数
- 为
getUserName
函数创建一个桩,使其返回“Test User”而不是调用真实实现 - 调用我们正在测试的函数,它将使用打桩后的实现
- 验证结果符合我们的预期
- 恢复原始函数以防止影响其他测试
在测试场景中使用桩 跳转到标题
让我们看一个更实际的示例,一个与数据库交互的 UserRepository
类
import { assertSpyCalls, returnsNext, stub } from "jsr:@std/testing/mock";
import { assertThrows } from "jsr:@std/assert";
type User = {
id: number;
name: string;
};
// This represents our database access layer
const database = {
getUserById(id: number): User | undefined {
// In a real app, this would query a database
return { id, name: "Ada Lovelace" };
},
};
// The class we want to test
class UserRepository {
static findOrThrow(id: number): User {
const user = database.getUserById(id);
if (!user) {
throw new Error("User not found");
}
return user;
}
}
Deno.test("findOrThrow method throws when the user was not found", () => {
// Stub the database.getUserById function to return undefined
using dbStub = stub(database, "getUserById", returnsNext([undefined]));
// We expect this function call to throw an error
assertThrows(() => UserRepository.findOrThrow(1), Error, "User not found");
// Verify the stubbed function was called once
assertSpyCalls(dbStub, 1);
});
在此示例中
- 我们正在测试
findOrThrow
方法,当未找到用户时,它应该抛出错误 - 我们将
database.getUserById
打桩为返回undefined
,模拟用户缺失的情况 - 我们验证
findOrThrow
抛出了预期的错误 - 我们还检查了数据库方法是否被精确调用了一次
请注意,我们对 stub
使用了 using
关键字,这是一种方便的方式,可以确保桩在超出范围时自动恢复。
高级桩技术 跳转到标题
在后续调用中返回不同值 跳转到标题
有时你希望桩在每次调用时返回不同的值
import { returnsNext, stub } from "jsr:@std/testing/mock";
import { assertEquals } from "jsr:@std/assert";
Deno.test("stub with multiple return values", () => {
const fetchDataStub = stub(
globalThis,
"fetchData",
// Return these values in sequence
returnsNext(["first result", "second result", "third result"]),
);
try {
assertEquals(fetchData(), "first result");
assertEquals(fetchData(), "second result");
assertEquals(fetchData(), "third result");
} finally {
fetchDataStub.restore();
}
});
使用实现逻辑进行打桩 跳转到标题
你还可以在桩实现中提供自定义逻辑
import { stub } from "jsr:@std/testing/mock";
import { assertEquals } from "jsr:@std/assert";
Deno.test("stub with custom implementation", () => {
// Create a counter to track how many times the stub is called
let callCount = 0;
const calculateStub = stub(
globalThis,
"calculate",
(a: number, b: number) => {
callCount++;
return a + b * 2; // Custom implementation
},
);
try {
const result = calculate(5, 10);
assertEquals(result, 25); // 5 + (10 * 2)
assertEquals(callCount, 1);
} finally {
calculateStub.restore();
}
});
打桩API调用和外部服务 跳转到标题
桩最常见的用途之一是在测试期间替换API调用
import { assertEquals } from "jsr:@std/assert";
import { stub } from "jsr:@std/testing/mock";
async function fetchUserData(id: string) {
const response = await fetch(`https://api.example.com/users/${id}`);
if (!response.ok) {
throw new Error(`Failed to fetch user: ${response.status}`);
}
return await response.json();
}
Deno.test("fetchUserData with stubbed fetch", async () => {
const mockResponse = new Response(
JSON.stringify({ id: "123", name: "Jane Doe" }),
{ status: 200, headers: { "Content-Type": "application/json" } },
);
// Replace global fetch with a stubbed version
const fetchStub = stub(
globalThis,
"fetch",
() => Promise.resolve(mockResponse),
);
try {
const user = await fetchUserData("123");
assertEquals(user, { id: "123", name: "Jane Doe" });
} finally {
fetchStub.restore();
}
});
最佳实践 跳转到标题
-
始终恢复桩:使用
try/finally
块或using
关键字来确保桩被恢复,即使测试失败也不例外。 -
将桩用于外部依赖项:对数据库调用、API请求或文件系统操作进行打桩,使测试更快、更可靠。
-
保持桩简单:桩应该返回可预测的值,以便你可以测试特定场景。
-
必要时与侦察结合使用:有时你需要同时替换功能(桩)和跟踪调用(侦察)。
-
在正确的级别进行打桩:在接口边界处进行打桩,而不是深入到实现细节中。
🦕 桩是测试期间隔离代码的强大工具,允许你创建确定性的测试环境并轻松测试边缘情况。通过用受控行为替换真实实现,你可以编写更专注、更可靠的测试,这些测试运行快速且一致。
有关更多测试资源,请查看