deno.com
本页内容

测试中的打桩

打桩是一种强大的技术,它通过用受控的实现替换函数来隔离你正在测试的代码。虽然侦察(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();
  }
});

在此示例中,我们

  1. 从 Deno 的标准库导入必要的函数
  2. getUserName 函数创建一个桩,使其返回“Test User”而不是调用真实实现
  3. 调用我们正在测试的函数,它将使用打桩后的实现
  4. 验证结果符合我们的预期
  5. 恢复原始函数以防止影响其他测试

在测试场景中使用桩 跳转到标题

让我们看一个更实际的示例,一个与数据库交互的 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);
});

在此示例中

  1. 我们正在测试 findOrThrow 方法,当未找到用户时,它应该抛出错误
  2. 我们将 database.getUserById 打桩为返回 undefined,模拟用户缺失的情况
  3. 我们验证 findOrThrow 抛出了预期的错误
  4. 我们还检查了数据库方法是否被精确调用了一次

请注意,我们对 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();
  }
});

最佳实践 跳转到标题

  1. 始终恢复桩:使用 try/finally 块或 using 关键字来确保桩被恢复,即使测试失败也不例外。

  2. 将桩用于外部依赖项:对数据库调用、API请求或文件系统操作进行打桩,使测试更快、更可靠。

  3. 保持桩简单:桩应该返回可预测的值,以便你可以测试特定场景。

  4. 必要时与侦察结合使用:有时你需要同时替换功能(桩)和跟踪调用(侦察)。

  5. 在正确的级别进行打桩:在接口边界处进行打桩,而不是深入到实现细节中。

🦕 桩是测试期间隔离代码的强大工具,允许你创建确定性的测试环境并轻松测试边缘情况。通过用受控行为替换真实实现,你可以编写更专注、更可靠的测试,这些测试运行快速且一致。

有关更多测试资源,请查看

您找到所需内容了吗?

隐私政策