在本页

快照测试

Deno 标准库带有一个 snapshot 模块,它使开发人员能够编写测试,这些测试将值断言为参考快照。此参考快照是原始值的序列化表示,并与测试文件一起存储。

快照测试在许多情况下都很有用,因为它能够用很少的代码捕获各种错误。它在难以精确表达应断言的内容的情况下特别有用,而无需大量代码,或者测试做出的断言预计会经常更改。因此,它特别适合用于前端和 CLI 的开发。

基本用法 跳转到标题

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

example_test.ts
import {
  assertSnapshot,
} from "https://deno.land/std@0.224.0/testing/snapshot.ts";

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

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

如果在测试期间创建的快照与快照文件中的快照不匹配,则在测试中调用 assertSnapshot 将抛出 AssertionError,导致测试失败。

创建和更新快照 跳转到标题

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

deno test --allow-all -- --update

此外,只有在存在此标志时才会创建新的快照。

权限 跳转到标题

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

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

版本控制 跳转到标题

当快照文件的更改与其他代码更改一起提交时,快照测试效果最佳。这允许对参考快照的更改与导致它们的代码更改一起进行审查,并确保当其他人拉取您的更改时,他们的测试将通过,而无需在本地更新快照。

高级用法 跳转到标题

选项 跳转到标题

assertSnapshot 函数也可以使用提供更大灵活性和启用一些非标准用例的选项对象调用。

import {
  assertSnapshot,
} from "https://deno.land/std@0.224.0/testing/snapshot.ts";

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

序列化器

serializer 选项允许您提供自定义序列化器函数。这将由 assertSnapshot 调用,并传递要断言的值。它应该返回一个字符串。重要的是,序列化器函数是确定性的,即它在给定相同输入的情况下始终产生相同的输出。

序列化器函数的结果将在更新模式下写入快照文件,并在断言模式下与存储在快照文件中的快照进行比较。

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

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

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

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

自定义序列化器在各种情况下都很有用。一个可能的用例是丢弃不相关的信息,或以更易于人类阅读的形式呈现序列化输出。

例如,上面的代码片段展示了如何使用自定义序列化器来删除 ANSI 转义码(在 CLI 应用程序中编码字体颜色和样式),使快照比平时更易于阅读。

其他常见的用例是混淆非确定性值或出于其他原因不想写入磁盘的值。例如,时间戳或文件路径。

请注意,默认序列化器是从快照模块导出的,以便可以轻松扩展其功能。

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 选项可以是 assertupdate。设置后,--update-u 标志将被忽略。

如果 mode 选项设置为 assert,则 assertSnapshot 将始终表现得好像没有传递更新标志一样,即如果快照与保存在快照文件中的快照不匹配,则将抛出 AssertionError

如果 mode 选项设置为 update,则 assertSnapshot 将始终表现得好像传递了更新标志一样,即如果快照与保存在快照文件中的快照不匹配,则在所有测试运行后将更新快照。

name

name 选项指定快照的名称。默认情况下,将使用测试步骤的名称。但是,如果指定,则将使用 name 选项。

example_test.ts
import {
  assertSnapshot,
} from "https://deno.land/std@0.224.0/testing/snapshot.ts";

Deno.test("isSnapshotMatch", async function (t): Promise<void> {
  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 使用相同的 name 值多次运行时,后缀将按正常方式递增。即 Test Name 1Test Name 2Test Name 3 等。

msg

允许设置自定义错误消息以使用。这将覆盖默认错误消息,其中包括失败快照的差异。

默认选项 跳转到标题

您可以为 assertSnapshot 配置默认选项。

example_test.ts
import {
  createAssertSnapshot,
} from "https://deno.land/std@0.224.0/testing/snapshot.ts";

const assertSnapshot = createAssertSnapshot({
  // options
});

当像这样配置默认选项时,生成的 assertSnapshot 函数将与从快照模块导出的默认函数具有相同的功能。如果传递可选的选项对象,这将优先于默认选项,其中为选项提供的值不同。

可以“扩展”已使用默认选项配置的 assertSnapshot 函数。

example_test.ts
import {
  createAssertSnapshot,
} from "https://deno.land/std@0.224.0/testing/snapshot.ts";
import { stripColor } from "https://deno.land/std@0.224.0/fmt/colors.ts";

const assertSnapshot = createAssertSnapshot({
  dir: ".snaps",
});

const assertMonochromeSnapshot = createAssertSnapshot<string>(
  { serializer: stripColor },
  assertSnapshot,
);

Deno.test("isSnapshotMatch", async function (t): Promise<void> {
  const a = "\x1b[32mThis green text has had it's colours stripped\x1b[39m";
  await assertMonochromeSnapshot(t, a);
});
.snaps/example_test.ts.snap
export const snapshot = {};

snapshot[`isSnapshotMatch 1`] = `This green text has had it's colours stripped`;

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

可以通过两种方式自定义默认序列化行为。第一种是通过指定 serializer 选项。这允许您控制传递给特定 assertSnapshot 调用的任何值的序列化。请参阅上面的文档,了解此选项的正确用法。

第二个选项是使用 Deno.customInspect。因为 assertSnapshot 使用的默认序列化器在幕后使用 Deno.inspect,所以您可以将属性 Symbol.for("Deno.customInspect") 设置为自定义序列化函数。

这样做将确保默认情况下,每当将对象传递给 assertSnapshot 时,都会使用自定义序列化。这在许多情况下都很有用。下面的代码片段中显示了一个示例。

example_test.ts
import {
  assertSnapshot,
} from "https://deno.land/std@0.224.0/testing/snapshot.ts";

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", [
        "Ex of customInspect for a snapshot of an 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>
      Ex of customInspect for a snapshot of an 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: [
            "Ex of customInspect for a snapshot of an SSR representation",
          ],
          name: "p",
        },
      ],
      name: "body",
    },
  ],
  name: "html",
}
`;

您可以看到此快照的可读性要差得多。这是因为

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

请注意,在这个例子中,通过调用可以完全实现相同的结果。

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

但是,根据您选择公开的公共 API,在其他情况下这可能不切实际。

还值得考虑的是,这将对快照测试之外的其他方面产生影响。例如,Deno.customInspect 也用于在调用 console.log 和其他一些情况下序列化对象。这可能是可取的,也可能不可取。