跳至主要内容

快照测试

Deno 标准库提供了一个 快照模块,它允许开发人员编写测试,这些测试将值与参考快照进行断言。此参考快照是原始值的序列化表示,并存储在测试文件旁边。

快照测试在许多情况下都很有用,因为它可以帮助用很少的代码捕获各种错误。它在难以精确表达断言内容、需要大量代码或断言内容经常变化的情况下特别有用。因此,它特别适合用于前端和 CLI 的开发。

基本用法

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

example_test.ts
import {
assertSnapshot,
} from "https://deno.land/[email protected]/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/[email protected]/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/[email protected]/testing/snapshot.ts";
import { stripColor } from "https://deno.land/[email protected]/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/[email protected]/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/[email protected]/testing/snapshot.ts";

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

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

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

example_test.ts
import {
createAssertSnapshot,
} from "https://deno.land/[email protected]/testing/snapshot.ts";
import { stripColor } from "https://deno.land/[email protected]/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/[email protected]/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和其他一些情况下序列化对象。这可能是可取的,也可能不可取。