快照測試保存值的輸出並將其與未來的測試運行進行比較。這對於 UI 組件、復雜對象或任何需要保持一致的輸出特別有用。
基本快照
快照測試使用 .toMatchSnapshot() 匹配器編寫:
ts
import { test, expect } from "bun:test";
test("snap", () => {
expect("foo").toMatchSnapshot();
});第一次運行此測試時,expect 的參數將被序列化並寫入測試文件旁邊 __snapshots__ 目錄中的特殊快照文件。
快照文件
運行上述測試後,Bun 將創建:
text
your-project/
├── snap.test.ts
└── __snapshots__/
└── snap.test.ts.snap快照文件包含:
ts
// Bun Snapshot v1, https://bun.com/docs/test/snapshots
exports[`snap 1`] = `"foo"`;在未來的運行中,參數將與磁盤上的快照進行比較。
更新快照
可以使用以下命令重新生成快照:
bash
bun test --update-snapshots這在以下情況下很有用:
- 你已故意更改了輸出
- 你正在添加新的快照測試
- 預期輸出已合法更改
內聯快照
對於較小的值,你可以使用 .toMatchInlineSnapshot() 進行內聯快照。這些快照直接存儲在你的測試文件中:
ts
import { test, expect } from "bun:test";
test("內聯快照", () => {
// 第一次運行:快照將自動插入
expect({ hello: "world" }).toMatchInlineSnapshot();
});第一次運行後,Bun 自動更新你的測試文件:
ts
import { test, expect } from "bun:test";
test("內聯快照", () => {
expect({ hello: "world" }).toMatchInlineSnapshot(`
{
"hello": "world",
}
`);
});使用內聯快照
- 使用
.toMatchInlineSnapshot()編寫測試 - 運行測試一次
- Bun 自動使用快照更新你的測試文件
- 在後續運行中,值將與內聯快照進行比較
內聯快照對於小而簡單的值特別有用,在測試文件中直接看到預期輸出很有幫助。
錯誤快照
你還可以使用 .toThrowErrorMatchingSnapshot() 和 .toThrowErrorMatchingInlineSnapshot() 對錯誤消息進行快照:
ts
import { test, expect } from "bun:test";
test("錯誤快照", () => {
expect(() => {
throw new Error("Something went wrong");
}).toThrowErrorMatchingSnapshot();
expect(() => {
throw new Error("Another error");
}).toThrowErrorMatchingInlineSnapshot();
});運行後,內聯版本變為:
ts
test("錯誤快照", () => {
expect(() => {
throw new Error("Something went wrong");
}).toThrowErrorMatchingSnapshot();
expect(() => {
throw new Error("Another error");
}).toThrowErrorMatchingInlineSnapshot(`"Another error"`);
});高級快照用法
復雜對象
快照適用於復雜的嵌套對象:
ts
import { test, expect } from "bun:test";
test("復雜對象快照", () => {
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();
});數組快照
數組也適合快照測試:
ts
import { test, expect } from "bun:test";
test("數組快照", () => {
const numbers = [1, 2, 3, 4, 5].map(n => n * 2);
expect(numbers).toMatchSnapshot();
});函數輸出快照
對函數輸出進行快照:
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("報告生成", () => {
const data = [
{ id: 1, name: "Alice", age: 30 },
{ id: 2, name: "Bob", age: 25 },
];
expect(generateReport(data)).toMatchSnapshot();
});React 組件快照
快照對於 React 組件特別有用:
tsx
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 組件快照", () => {
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),使用屬性匹配器:
ts
import { test, expect } from "bun:test";
test("具有動態值的快照", () => {
const user = {
id: Math.random(), // 每次運行都會變化
name: "John",
createdAt: new Date().toISOString(), // 這也會變化
};
expect(user).toMatchSnapshot({
id: expect.any(Number),
createdAt: expect.any(String),
});
});快照將存儲:
txt
exports[`snapshot with dynamic values 1`] = `
{
"createdAt": Any<String>,
"id": Any<Number>,
"name": "John",
}
`;自定義序列化器
你可以自定義對象在快照中的序列化方式:
ts
import { test, expect } from "bun:test";
// Date 對象的自定義序列化器
expect.addSnapshotSerializer({
test: val => val instanceof Date,
serialize: val => `"${val.toISOString()}"`,
});
test("自定義序列化器", () => {
const event = {
name: "Meeting",
date: new Date("2024-01-01T10:00:00Z"),
};
expect(event).toMatchSnapshot();
});最佳實踐
保持快照小巧
ts
// 好:專注的快照
test("用戶姓名格式化", () => {
const formatted = formatUserName("john", "doe");
expect(formatted).toMatchInlineSnapshot(`"John Doe"`);
});
// 避免:難以審查的巨大快照
test("整個頁面渲染", () => {
const page = renderEntirePage();
expect(page).toMatchSnapshot(); // 這可能是數千行
});使用描述性測試名稱
ts
// 好:清楚快照代表什麼
test("使用 USD 符號格式化貨幣", () => {
expect(formatCurrency(99.99)).toMatchInlineSnapshot(`"$99.99"`);
});
// 避免:不清楚測試的是什麼
test("格式化測試", () => {
expect(format(99.99)).toMatchInlineSnapshot(`"$99.99"`);
});分組相關快照
ts
import { describe, test, expect } from "bun:test";
describe("Button 組件", () => {
test("主要變體", () => {
expect(render(<Button variant="primary">Click</Button>))
.toMatchSnapshot();
});
test("次要變體", () => {
expect(render(<Button variant="secondary">Cancel</Button>))
.toMatchSnapshot();
});
test("禁用狀態", () => {
expect(render(<Button disabled>Disabled</Button>))
.toMatchSnapshot();
});
});處理動態數據
ts
// 好:規范化動態數據
test("API 響應格式", () => {
const response = {
data: { id: 1, name: "Test" },
timestamp: Date.now(),
requestId: generateId(),
};
expect({
...response,
timestamp: "TIMESTAMP",
requestId: "REQUEST_ID",
}).toMatchSnapshot();
});
// 或使用屬性匹配器
test("使用匹配器的 API 響應", () => {
const response = getApiResponse();
expect(response).toMatchSnapshot({
timestamp: expect.any(Number),
requestId: expect.any(String),
});
});管理快照
審查快照更改
當快照更改時,仔細審查它們:
bash
# 查看更改內容
git diff __snapshots__/
# 如果更改是有意的則更新
bun test --update-snapshots
# 提交更新的快照
git add __snapshots__/
git commit -m "UI 更改後更新快照"清理未使用的快照
Bun 將警告未使用的快照:
txt
Warning: 1 unused snapshot found:
my-test.test.ts.snap: "old test that no longer exists 1"通過從快照文件中刪除它們或在可用時使用清理標志運行測試來刪除未使用的快照。
組織大型快照文件
對於大型項目,考慮組織測試以保持快照文件易於管理:
text
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更新) - 無意的更改(修復代碼)
- 動態數據(使用屬性匹配器)
- 環境差異(規范化數據)
平台差異
注意特定於平台的差異:
ts
// Windows/Unix 之間的路徑可能不同
test("文件操作", () => {
const result = processFile("./test.txt");
expect({
...result,
path: result.path.replace(/\\/g, "/"), // 規范化路徑
}).toMatchSnapshot();
});