Skip to main content
快照测试保存一个值的输出并与未来的测试运行进行比较。这对于 UI 组件、复杂对象或任何需要保持一致的输出特别有用。

基础快照

快照测试使用 .toMatchSnapshot() 匹配器编写:
https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2test.ts
import { test, expect } from "bun:test";

test("snap", () => {
  expect("foo").toMatchSnapshot();
});
第一次运行此测试时,expect 的参数将被序列化并写入测试文件旁边的 __snapshots__ 目录中的特殊快照文件。

快照文件

运行上面的测试后,Bun 将创建:
directory structure
your-project/
├── snap.test.ts
└── __snapshots__/
    └── snap.test.ts.snap
快照文件包含:
__snapshots__/snap.test.ts.snap
// Bun Snapshot v1, https://bun.com/docs/test/snapshots

exports[`snap 1`] = `"foo"`;
在后续运行中,参数将与磁盘上的快照进行比较。

更新快照

可以使用以下命令重新生成快照:
terminal
bun test --update-snapshots
这在以下情况下很有用:
  • 您有意更改了输出
  • 您正在添加新的快照测试
  • 预期输出确实发生了变化

内联快照

对于较小的值,您可以使用 .toMatchInlineSnapshot() 的内联快照。这些快照直接存储在您的测试文件中:
https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2test.ts
import { test, expect } from "bun:test";

test("inline snapshot", () => {
  // 第一次运行:快照将自动插入
  expect({ hello: "world" }).toMatchInlineSnapshot();
});
在第一次运行后,Bun 自动更新您的测试文件:
https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2test.ts
import { test, expect } from "bun:test";

test("inline snapshot", () => {
  expect({ hello: "world" }).toMatchInlineSnapshot(`
{
  "hello": "world",
}
`);
});

使用内联快照

  1. 使用 .toMatchInlineSnapshot() 编写测试
  2. 运行一次测试
  3. Bun 自动使用快照更新您的测试文件
  4. 在后续运行中,值将与内联快照进行比较
内联快照对于小的、简单的值特别有用,在测试文件中直接看到预期输出很有帮助。

错误快照

您还可以使用 .toThrowErrorMatchingSnapshot().toThrowErrorMatchingInlineSnapshot() 快照错误消息:
https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2test.ts
import { test, expect } from "bun:test";

test("error snapshot", () => {
  expect(() => {
    throw new Error("Something went wrong");
  }).toThrowErrorMatchingSnapshot();

  expect(() => {
    throw new Error("Another error");
  }).toThrowErrorMatchingInlineSnapshot();
});
运行后,内联版本变为:
https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2test.ts
test("error snapshot", () => {
  expect(() => {
    throw new Error("Something went wrong");
  }).toThrowErrorMatchingSnapshot();

  expect(() => {
    throw new Error("Another error");
  }).toThrowErrorMatchingInlineSnapshot(`"Another error"`);
});

高级快照用法

复杂对象

快照适用于复杂的嵌套对象:
https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2test.ts
import { test, expect } from "bun:test";

test("complex object snapshot", () => {
  const user = {
    id: 1,
    name: "John Doe",
    email: "john@example.com",
    profile: {
      age: 30,
      preferences: {
        theme: "dark",
        notifications: true,
      },
    },
    tags: ["developer", "javascript", "bun"],
  };

  expect(user).toMatchSnapshot();
});

数组快照

数组也非常适合快照测试:
https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2test.ts
import { test, expect } from "bun:test";

test("array snapshot", () => {
  const numbers = [1, 2, 3, 4, 5].map(n => n * 2);
  expect(numbers).toMatchSnapshot();
});

函数输出快照

快照函数的输出:
https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2test.ts
import { test, expect } from "bun:test";

function generateReport(data: any[]) {
  return {
    total: data.length,
    summary: data.map(item => ({ id: item.id, name: item.name })),
    timestamp: "2024-01-01", // 为测试固定
  };
}

test("report generation", () => {
  const data = [
    { id: 1, name: "Alice", age: 30 },
    { id: 2, name: "Bob", age: 25 },
  ];

  expect(generateReport(data)).toMatchSnapshot();
});

React 组件快照

快照对于 React 组件特别有用:
https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2test.ts
import { test, expect } from "bun:test";
import { render } from "@testing-library/react";

function Button({ children, variant = "primary" }) {
  return <button className={`btn btn-${variant}`}>{children}</button>;
}

test("Button component snapshots", () => {
  const { container: primary } = render(<Button>Click me</Button>);
  const { container: secondary } = render(<Button variant="secondary">Cancel</Button>);

  expect(primary.innerHTML).toMatchSnapshot();
  expect(secondary.innerHTML).toMatchSnapshot();
});

属性匹配器

对于在测试运行之间更改的值(如时间戳或 ID),请使用属性匹配器:
https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2test.ts
import { test, expect } from "bun:test";

test("snapshot with dynamic values", () => {
  const user = {
    id: Math.random(), // 这在每次运行时都改变
    name: "John",
    createdAt: new Date().toISOString(), // 这也改变
  };

  expect(user).toMatchSnapshot({
    id: expect.any(Number),
    createdAt: expect.any(String),
  });
});
快照将存储:
snapshot file
exports[`snapshot with dynamic values 1`] = `
{
  "createdAt": Any<String>,
  "id": Any<Number>,
  "name": "John",
}
`;

自定义序列化器

您可以自定义对象在快照中的序列化方式:
https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2test.ts
import { test, expect } from "bun:test";

// Date 对象的自定义序列化器
expect.addSnapshotSerializer({
  test: val => val instanceof Date,
  serialize: val => `"${val.toISOString()}"`,
});

test("custom serializer", () => {
  const event = {
    name: "Meeting",
    date: new Date("2024-01-01T10:00:00Z"),
  };

  expect(event).toMatchSnapshot();
});

最佳实践

保持快照小

https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2test.ts
// 好:集中的快照
test("user name formatting", () => {
  const formatted = formatUserName("john", "doe");
  expect(formatted).toMatchInlineSnapshot(`"John Doe"`);
});

// 避免:巨大的快照,难以审查
test("entire page render", () => {
  const page = renderEntirePage();
  expect(page).toMatchSnapshot(); // 这可能是数千行
});

使用描述性测试名称

https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2test.ts
// 好:清楚快照代表什么
test("formats currency with USD symbol", () => {
  expect(formatCurrency(99.99)).toMatchInlineSnapshot(`"$99.99"`);
});

// 避免:不清楚测试的是什么
test("format test", () => {
  expect(format(99.99)).toMatchInlineSnapshot(`"$99.99"`);
});

分组相关快照

https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2test.ts
import { describe, test, expect } from "bun:test";

describe("Button component", () => {
  test("primary variant", () => {
    expect(render(<Button variant="primary">Click</Button>))
      .toMatchSnapshot();
  });

  test("secondary variant", () => {
    expect(render(<Button variant="secondary">Cancel</Button>))
      .toMatchSnapshot();
  });

  test("disabled state", () => {
    expect(render(<Button disabled>Disabled</Button>))
      .toMatchSnapshot();
  });
});

处理动态数据

https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2test.ts
// 好:标准化动态数据
test("API response format", () => {
  const response = {
    data: { id: 1, name: "Test" },
    timestamp: Date.now(),
    requestId: generateId(),
  };

  expect({
    ...response,
    timestamp: "TIMESTAMP",
    requestId: "REQUEST_ID",
  }).toMatchSnapshot();
});

// 或使用属性匹配器
test("API response with matchers", () => {
  const response = getApiResponse();

  expect(response).toMatchSnapshot({
    timestamp: expect.any(Number),
    requestId: expect.any(String),
  });
});

管理快照

审查快照更改

当快照更改时,仔细审查它们:
terminal
# 查看更改内容
git diff __snapshots__/

# 如果更改是有意的则更新
bun test --update-snapshots

# 提交更新的快照
git add __snapshots__/
git commit -m "Update snapshots after UI changes"

清理未使用的快照

Bun 将警告未使用的快照:
warning
Warning: 1 unused snapshot found:
  my-test.test.ts.snap: "old test that no longer exists 1"
通过从快照文件中删除它们或运行带有清理标志的测试(如果可用)来删除未使用的快照。

组织大型快照文件

对于大型项目,考虑组织测试以保持快照文件可管理:
directory structure
tests/
├── components/
│   ├── Button.test.tsx
│   └── __snapshots__/
│       └── Button.test.tsx.snap
├── utils/
│   ├── formatters.test.ts
│   └── __snapshots__/
│       └── formatters.test.ts.snap

故障排除

快照失败

当快照失败时,您将看到差异:
diff
- Expected
+ Received

  Object {
-   "name": "John",
+   "name": "Jane",
  }
常见原因:
  • 有意更改(使用 --update-snapshots 更新)
  • 意外更改(修复代码)
  • 动态数据(使用属性匹配器)
  • 环境差异(标准化数据)

平台差异

注意平台特定的差异:
https://mintcdn.com/teemo/2s-4Z6VdGqiCeBNX/icons/typescript.svg?fit=max&auto=format&n=2s-4Z6VdGqiCeBNX&q=85&s=087b260066909db1cd3e9c7292bc34b2test.ts
// 路径在 Windows/Unix 之间可能不同
test("file operations", () => {
  const result = processFile("./test.txt");

  expect({
    ...result,
    path: result.path.replace(/\\/g, "/"), // 标准化路径
  }).toMatchSnapshot();
});