deno.com
本页内容

快照测试

快照测试是一种测试技术,它捕获代码的输出并将其与存储的参考版本进行比较。您无需手动为每个属性编写断言,而是让测试运行器记录整个输出结构,从而更容易检测到任何意外更改。

Deno 标准库包含一个快照模块,该模块使开发者能够编写测试,断言一个值与参考快照是否一致。这个参考快照是原始值的序列化表示,并与测试文件一同存储。

基本用法 跳转到标题

assertSnapshot 函数将创建一个值的快照,并将其与存储在测试文件旁边的 __snapshots__ 目录中的参考快照进行比较。

要创建初始快照(或更新现有快照),请在 deno test 命令中使用 -- --update 标志。

基本快照示例 跳转到标题

以下示例展示了如何将快照库与 Deno.test API 一起使用。我们可以测试一个包含字符串和数字属性的基本对象的快照。

assertSnapshot(t, a) 函数将对象与存储的快照进行比较。t 参数是 Deno 提供的测试上下文,快照函数使用该上下文来确定测试名称和存储快照的位置。

example_test.ts
import { assertSnapshot } from "jsr:@std/testing/snapshot";

Deno.test("isSnapshotMatch", async (t) => {
  const a = {
    hello: "world!",
    example: 123,
  };
  await assertSnapshot(t, a);
});

您需要授予读写文件权限,以便 Deno 能够写入快照文件,然后读取它以测试断言。如果您是第一次运行测试且尚未拥有快照,请添加 --update 标志

deno test --allow-read --allow-write -- --update

如果您已经有快照文件,您可以通过以下方式运行测试

deno test --allow-read

测试会将对象的当前输出与存储的快照进行比较。如果它们匹配,则测试通过;如果它们不同,则测试失败。

快照文件将如下所示

__snapshots__/example_test.ts.snap
export const snapshot = {};

snapshot[`isSnapshotMatch 1`] = `
{
  example: 123,
  hello: "world!",
}
`;

您可以编辑测试,将 hello 字符串更改为 "everyone!",然后再次使用 deno test --allow-read 运行测试。这次 assertSnapshot 函数将抛出 AssertionError,导致测试失败,因为在测试期间创建的快照与快照文件中的快照不匹配。

更新快照 跳转到标题

当向测试套件添加新的快照断言时,或有意进行导致快照失败的更改时,您可以通过在更新模式下运行快照测试来更新快照。可以通过在运行测试时传递 --update-u 标志作为参数来在更新模式下运行测试。当传递此标志时,任何不匹配的快照都将被更新。

deno test --allow-read --allow-write -- --update

注意

只有当 --update 标志存在时,才会创建新快照。

权限 跳转到标题

运行快照测试时,必须启用 --allow-read 权限,否则对 assertSnapshot 的任何调用都将因权限不足而失败。此外,更新快照时,必须启用 --allow-write 权限,因为这是更新快照文件所必需的。

assertSnapshot 函数只会尝试读取和写入快照文件。因此,如果需要,--allow-read--allow-write 的允许列表可以限制为仅包含现有快照文件。

版本控制 跳转到标题

快照测试在快照文件的更改与其他代码更改一同提交时效果最佳。这使得对参考快照的更改可以与导致它们的代码更改一同审查,并确保当其他人拉取您的更改时,他们的测试无需在本地更新快照即可通过。

选项 跳转到标题

assertSnapshot 函数可以使用 options 对象调用,该对象提供了更大的灵活性,并支持一些非标准用例

import { assertSnapshot } from "jsr:@std/testing/snapshot";

Deno.test("isSnapshotMatch", async (t) => {
  const a = {
    hello: "world!",
    example: 123,
  };
  await assertSnapshot(t, a, {/*custom options go here*/});
});

序列化器 跳转到标题

当您使用 assertSnapshot 运行测试时,您正在测试的数据需要转换为字符串格式,以便写入快照文件(创建或更新快照时)并与现有快照进行比较(验证时),这称为序列化。

serializer 选项允许您提供自定义序列化函数。assertSnapshot 将调用此自定义函数,并传入正在断言的值。您的自定义函数必须

  1. 返回一个 string
  2. 是确定性的(给定相同的输入,它将始终产生相同的输出)。

以下代码展示了为快照测试创建和使用自定义序列化函数的实际示例。此序列化器使用 Deno 标准库中的stripColour 字符串格式化工具从字符串中移除任何 ANSI 颜色代码。

example_test.ts
import { assertSnapshot, serialize } from "jsr:@std/testing/snapshot";
import { stripColor } from "jsr:@std/fmt/colors";

/**
 * Serializes `actual` and removes ANSI escape codes.
 */
function customSerializer(actual: string) {
  return serialize(stripColor(actual));
}

Deno.test("Custom Serializer", async (t) => {
  const output = "\x1b[34mHello World!\x1b[39m";
  await assertSnapshot(t, output, {
    serializer: customSerializer,
  });
});
__snapshots__/example_test.ts.snap
snapshot = {};

snapshot[`Custom Serializer 1`] = `"Hello World!"`;

自定义序列化器在多种场景下都很有用

  • 移除不相关的格式(如上面所示的 ANSI 代码)并提高可读性
  • 处理非确定性数据。时间戳、UUID 或随机值可以用占位符替换
  • 掩盖或移除不应保存在快照中的敏感数据
  • 自定义格式以领域特定格式呈现复杂对象

使用 Deno.customInspect 进行序列化 跳转到标题

因为默认的序列化器在底层使用 Deno.inspect,所以如果需要,您可以将属性 Symbol.for("Deno.customInspect") 设置为自定义序列化函数

example_test.ts
// example_test.ts
import { assertSnapshot } from "jsr:@std/testing/snapshot";

class HTMLTag {
  constructor(
    public name: string,
    public children: Array<HTMLTag | string> = [],
  ) {}

  public render(depth: number) {
    const indent = "  ".repeat(depth);
    let output = `${indent}<${this.name}>\n`;
    for (const child of this.children) {
      if (child instanceof HTMLTag) {
        output += `${child.render(depth + 1)}\n`;
      } else {
        output += `${indent}  ${child}\n`;
      }
    }
    output += `${indent}</${this.name}>`;
    return output;
  }

  public [Symbol.for("Deno.customInspect")]() {
    return this.render(0);
  }
}

Deno.test("Page HTML Tree", async (t) => {
  const page = new HTMLTag("html", [
    new HTMLTag("head", [
      new HTMLTag("title", [
        "Simple SSR Example",
      ]),
    ]),
    new HTMLTag("body", [
      new HTMLTag("h1", [
        "Simple SSR Example",
      ]),
      new HTMLTag("p", [
        "This is an example of how Deno.customInspect could be used to snapshot an intermediate SSR representation",
      ]),
    ]),
  ]);

  await assertSnapshot(t, page);
});

此测试将生成以下快照。

__snapshots__/example_test.ts.snap
export const snapshot = {};

snapshot[`Page HTML Tree 1`] = `
<html>
  <head>
    <title>
      Simple SSR Example
    </title>
  </head>
  <body>
    <h1>
      Simple SSR Example
    </h1>
    <p>
      This is an example of how Deno.customInspect could be used to snapshot an intermediate SSR representation
    </p>
  </body>
</html>
`;

相比之下,当我们移除 Deno.customInspect 方法时,测试将生成以下快照

__snapshots__/example_test.ts.snap
export const snapshot = {};

snapshot[`Page HTML Tree 1`] = `HTMLTag {
  children: [
    HTMLTag {
      children: [
        HTMLTag {
          children: [
            "Simple SSR Example",
          ],
          name: "title",
        },
      ],
      name: "head",
    },
    HTMLTag {
      children: [
        HTMLTag {
          children: [
            "Simple SSR Example",
          ],
          name: "h1",
        },
        HTMLTag {
          children: [
            "This is an example of how Deno.customInspect could be used to snapshot an intermediate SSR representation",
          ],
          name: "p",
        },
      ],
      name: "body",
    },
  ],
  name: "html",
}`;

您可以看到第二个快照的可读性差得多。这是因为

  1. 键按字母顺序排序,因此元素的名称显示在其子元素之后
  2. 它包含大量额外信息,导致快照长度增加一倍以上
  3. 它不是数据所代表的 HTML 的准确序列化

请注意,在此示例中,通过调用以下方式也可以达到相同的结果

await assertSnapshot(t, page.render(0));

然而,根据您选择公开的公共 API,这可能不切实际。

同样值得考虑的是,这可能对您的快照测试之外产生影响。例如,在调用 console.log 时(以及在某些其他情况下),Deno.customInspect 也用于序列化对象。这可能是期望的,也可能不是。

dirpath 跳转到标题

dirpath 选项允许您控制快照文件的保存和读取位置。这些可以是绝对路径或相对路径。如果是相对路径,它们将相对于测试文件解析。

例如,如果您的测试文件位于 /path/to/test.ts,并且 dir 选项设置为 snapshots,那么快照文件将写入 /path/to/snapshots/test.ts.snap

  • dir 允许您指定快照目录,同时仍使用快照文件名的默认格式。

  • path 允许您指定快照文件的目录和文件名。

如果您的测试文件位于 /path/to/test.ts,并且 path 选项设置为 snapshots/test.snapshot,那么快照文件将写入 /path/to/snapshots/test.snapshot

注意

如果同时指定了 dirpath,则 dir 选项将被忽略,path 选项将正常处理。

mode 跳转到标题

mode 选项控制 assertSnapshot 的行为方式,无论命令行标志如何,它有两种设置:assertupdate

  • assert:始终只执行比较,忽略任何 --update-u 标志。如果快照不匹配,测试将抛出 AssertionError 失败。

  • update:始终更新快照。任何不匹配的快照将在测试完成后更新。

当您需要在同一测试套件中实现不同的快照行为时,此选项非常有用。

// Create a new snapshot or verify an existing one
await assertSnapshot(t, stableComponent);

// Always update this snapshot regardless of command line flags
await assertSnapshot(t, experimentalComponent, {
  mode: "update",
  name: "experimental feature",
});

// Always verify but never update this snapshot regardless of command line flags
await assertSnapshot(t, criticalComponent, {
  mode: "assert",
  name: "critical feature",
});

name 跳转到标题

快照的名称。如果未指定,将使用测试步骤的名称。

example_test.ts
import { assertSnapshot } from "jsr:@std/testing/snapshot";

Deno.test("isSnapshotMatch", async (t) => {
  const a = {
    hello: "world!",
    example: 123,
  };
  await assertSnapshot(t, a, {
    name: "Test Name",
  });
});
__snapshots__/example_test.ts.snap
export const snapshot = {};

snapshot[`Test Name 1`] = `
{
  example: 123,
  hello: "world!",
}
`;

assertSnapshot 以相同的名称值多次运行时,后缀将正常递增,例如 Test Name 1Test Name 2Test Name 3 等。

msg 跳转到标题

用于设置自定义错误消息。这将覆盖默认错误消息,默认消息包含失败快照的差异。

Deno.test("custom error message example", async (t) => {
  const userData = {
    name: "John Doe",
    role: "admin",
  };

  await assertSnapshot(t, userData, {
    msg:
      "User data structure has changed unexpectedly. Please verify your changes are intentional.",
  });
});

当快照失败时,您将看到自定义错误消息,而不是默认的差异消息。

测试不同数据类型 跳转到标题

快照测试适用于各种数据类型和结构。

Deno.test("snapshot various types", async (t) => {
  // Arrays
  await assertSnapshot(t, [1, 2, 3, "four", { five: true }]);

  // Complex objects
  await assertSnapshot(t, {
    user: { name: "Test", roles: ["admin", "user"] },
    settings: new Map([["theme", "dark"], ["language", "en"]]),
  });

  // Error objects
  await assertSnapshot(t, new Error("Test error message"));
});

处理异步代码 跳转到标题

测试异步函数时,请确保在将结果传递给快照之前对其进行 await

Deno.test("async function test", async (t) => {
  const fetchData = async () => {
    // Simulate API call
    return { success: true, data: ["item1", "item2"] };
  };

  const result = await fetchData();
  await assertSnapshot(t, result);
});

最佳实践 跳转到标题

保持快照简洁 跳转到标题

避免捕获测试不需要的大型数据结构。专注于捕获相关内容即可。

描述性测试名称 跳转到标题

使用描述性测试名称,清楚地表明正在测试什么。

Deno.test(
  "renders user profile card with all required fields",
  async (t) => {
    // ... test code
    await assertSnapshot(t, component);
  },
);

在代码审查期间审查快照 跳转到标题

在代码审查期间始终审查快照更改,以确保它们代表有意更改而非回归。

快照组织 跳转到标题

对于大型项目,请考虑按功能或组件组织快照。

await assertSnapshot(t, component, {
  path: `__snapshots__/components/${componentName}.snap`,
});

CI/CD 中的快照测试 跳转到标题

GitHub Actions 示例 跳转到标题

在 CI 环境中运行快照测试时,您通常会希望验证现有快照而不是更新它们。

.github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: denoland/setup-deno@v2
        with:
          deno-version: v2.x
      - name: Run tests
        run: deno test --allow-read

对于有意更新快照的拉取请求,审阅者应在合并前验证更改是否符合预期。

实际示例 跳转到标题

测试 HTML 输出 跳转到标题

HTML 输出测试与快照结合对于 Web 应用程序特别有用,在这些应用程序中,您希望确保您的组件呈现预期的标记。此方法允许您捕获 HTML 结构、属性或内容中可能影响 UI 组件视觉外观或功能的意外更改。

通过捕获 HTML 输出的快照,您可以

  • 验证 UI 组件是否使用不同的属性/数据正确呈现
  • 重构渲染逻辑时检测回归
  • 记录组件的预期输出格式
Deno.test("HTML rendering", async (t) => {
  const renderComponent = () => {
    return `<div class="card">
      <h2>User Profile</h2>
      <p>Username: testuser</p>
    </div>`;
  };

  await assertSnapshot(t, renderComponent());
});

测试 API 响应 跳转到标题

在构建与 API 交互的应用程序时,快照测试有助于确保 API 响应的结构和格式保持一致。这对于以下情况尤其有价值:

  • 更新 API 集成时保持向后兼容性
  • 验证您的 API 响应解析逻辑是否正常工作
  • 为团队协作记录 API 响应的预期形状
  • 检测 API 响应中可能破坏应用程序的意外更改
Deno.test("API response format", async (t) => {
  const mockApiResponse = {
    status: 200,
    data: {
      users: [
        { id: 1, name: "User 1" },
        { id: 2, name: "User 2" },
      ],
      pagination: { page: 1, total: 10 },
    },
  };

  await assertSnapshot(t, mockApiResponse);
});

🦕 快照测试是一种强大的技术,它通过允许您捕获和验证复杂输出而无需编写详细断言来补充传统的单元测试。通过将快照测试纳入您的测试策略,您可以捕获意外更改、记录预期行为并构建更具弹性的应用程序。

您找到所需内容了吗?

隐私政策