快照测试
Deno 标准库提供了一个 快照模块,它允许开发人员编写测试,这些测试将值与参考快照进行断言。此参考快照是原始值的序列化表示,并存储在测试文件旁边。
快照测试在许多情况下都很有用,因为它可以帮助用很少的代码捕获各种错误。它在难以精确表达断言内容、需要大量代码或断言内容经常变化的情况下特别有用。因此,它特别适合用于前端和 CLI 的开发。
基本用法
assertSnapshot
函数会创建一个值的快照,并将其与存储在测试文件旁边 __snapshots__
目录中的参考快照进行比较。
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);
});
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
调用,并传递要断言的值。它应该返回一个字符串。重要的是,序列化器函数必须是确定性的,即对于相同的输入,它始终会产生相同的输出。
序列化器函数的结果将在更新模式下写入快照文件,并在断言模式下与快照文件中存储的快照进行比较。
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,
});
});
export const snapshot = {};
snapshot[`Custom Serializer 1`] = `"Hello World!"`;
自定义序列化器在各种情况下都很有用。一个可能的用例是丢弃不相关的信息,或以更易于人类阅读的形式呈现序列化输出。
例如,上面的代码片段展示了如何使用自定义序列化器来删除 ANSI 转义码(在 CLI 应用程序中编码字体颜色和样式),使快照比其他方式更易于阅读。
其他常见用例是模糊化非确定性值或出于其他原因不想写入磁盘的值。例如,时间戳或文件路径。
请注意,默认序列化器是从快照模块导出的,因此可以轻松扩展其功能。
dir
和 path
dir
和 path
选项允许您控制快照文件将保存到和读取的位置。这些可以是绝对路径或相对路径。如果为相对路径,则它们将相对于测试文件解析。
例如,如果您的测试文件位于 /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
。
如果同时指定了 dir
和 path
,则 dir
选项将被忽略,path
选项将按正常方式处理。
mode
mode
选项可以是 assert
或 update
。设置后,--update
和 -u
标志将被忽略。
如果 mode
选项设置为 assert
,则 assertSnapshot
将始终表现得好像没有传递更新标志一样,即如果快照与快照文件中保存的快照不匹配,则将抛出 AssertionError
。
如果 mode
选项设置为 update
,则 assertSnapshot
将始终表现得好像传递了更新标志一样,即如果快照与快照文件中保存的快照不匹配,则在所有测试运行后将更新快照。
name
name
选项指定快照的名称。默认情况下,将使用测试步骤的名称。但是,如果指定,则将使用 name
选项。
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",
});
});
export const snapshot = {};
snapshot[`Test Name 1`] = `
{
example: 123,
hello: "world!",
}
`;
当 assertSnapshot
使用 name
的相同值多次运行时,后缀将按正常方式递增。即 Test Name 1
、Test Name 2
、Test Name 3
等。
msg
允许设置自定义错误消息以使用。这将覆盖默认错误消息,其中包括失败快照的差异。
默认选项
您可以为 assertSnapshot
配置默认选项。
import {
createAssertSnapshot,
} from "https://deno.land/[email protected]/testing/snapshot.ts";
const assertSnapshot = createAssertSnapshot({
// options
});
当以这种方式配置默认选项时,生成的 assertSnapshot
函数将与从快照模块导出的默认函数具有相同的功能。如果传递一个可选的选项对象,这将优先于默认选项,其中为选项提供的值不同。
可以“扩展”已使用默认选项配置的 assertSnapshot
函数。
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);
});
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
时,都会使用自定义序列化。这在许多情况下可能很有用。下面的代码片段显示了一个示例。
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);
});
此测试将生成以下快照。
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
方法时,测试将产生以下快照。
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",
}
`;
您可以看到,此快照的可读性要差得多。这是因为
- 键按字母顺序排序,因此元素的名称显示在其子元素之后
- 它包含许多额外的信息,导致快照的长度增加了一倍以上
- 它不是对数据所代表的 HTML 的准确序列化
请注意,在本例中,可以通过调用以下方法来完全实现相同的结果
await assertSnapshot(t, page.render(0));
但是,根据您选择公开的公共 API,这在其他情况下可能不切实际。
还需要考虑的是,这将对快照测试之外产生影响。例如,Deno.customInspect
也用于在调用console.log
和其他一些情况下序列化对象。这可能是可取的,也可能不可取。