快照測試
學習如何在 Bun 中使用快照測試,在測試執行之間儲存並比較輸出
快照測試會儲存某個值的輸出,並將其與未來測試執行中的輸出進行比較。它適用於 UI 元件、複雜物件,或任何需要保持一致的輸出。
基本快照
使用 .toMatchSnapshot() 匹配器編寫快照測試:
import { test, expect } from "bun:test";
test("snap", () => {
expect("foo").toMatchSnapshot();
});
第一次執行此測試時,Bun 會將傳遞給 expect 的引數序列化,並將其寫入測試檔案旁 __snapshots__ 目錄中的快照檔案。
快照檔案
第一次執行後,Bun 會建立:
目錄結構
your-project/
├── snap.test.ts
└── __snapshots__/
└── snap.test.ts.snap
快照檔案內容如下:
__snapshots__/snap.test.ts.snap
// Bun 快照版本 1,https://bun.sh/docs/test/snapshots
exports[`snap 1`] = `"foo"`;
在之後的執行中,Bun 會將引數與磁碟上的快照進行比較。
更新快照
使用以下命令重新生成快照:
terminal
bun test --update-snapshots
當你有意更改輸出時,請執行此操作。在 CI 環境中,除非傳遞此標誌,否則 Bun 不會寫入新的快照。
內聯快照
對於較小的值,請使用 .toMatchInlineSnapshot()。Bun 會將內聯快照直接儲存在測試檔案中:
import { test, expect } from "bun:test";
test("inline snapshot", () => {
// 首次執行:快照將自動插入
expect({ hello: "world" }).toMatchInlineSnapshot();
});
首次執行後,Bun 會自動更新你的測試檔案:
import { test, expect } from "bun:test";
test("inline snapshot", () => {
expect({ hello: "world" }).toMatchInlineSnapshot(`
{
"hello": "world",
}
`);
});
使用內聯快照步驟
- 使用
.toMatchInlineSnapshot()編寫測試 - 執行一次測試
- Bun 會自動使用快照更新你的測試檔案
- 後續執行環境,Bun 會將該值與內聯快照進行比較
錯誤快照
你還可以使用 .toThrowErrorMatchingSnapshot() 和 .toThrowErrorMatchingInlineSnapshot() 對錯誤訊息進行快照:
import { test, expect } from "bun:test";
test("error snapshot", () => {
expect(() => {
throw new Error("Something went wrong");
}).toThrowErrorMatchingSnapshot();
expect(() => {
throw new Error("Another error");
}).toThrowErrorMatchingInlineSnapshot();
});
執行後,內聯版本變為:
test("error snapshot", () => {
expect(() => {
throw new Error("Something went wrong");
}).toThrowErrorMatchingSnapshot();
expect(() => {
throw new Error("Another error");
}).toThrowErrorMatchingInlineSnapshot(`"Another error"`);
});
高階快照用法
複雜物件
快照適用於複雜的巢狀物件:
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();
});
陣列快照
陣列同樣適合快照測試:
import { test, expect } from "bun:test";
test("array snapshot", () => {
const numbers = [1, 2, 3, 4, 5].map(n => n * 2);
expect(numbers).toMatchSnapshot();
});
函式輸出快照
快照函式的傳回值:
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 元件:
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),使用屬性匹配器:
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),
});
});
快照檔案儲存:
快照檔案
exports[`snapshot with dynamic values 1`] = `
{
"createdAt": Any<String>,
"id": Any<Number>,
"name": "John",
}
`;
最佳實踐
保持快照檔案簡潔
// 推薦:聚焦的快照
test("user name formatting", () => {
const formatted = formatUserName("john", "doe");
expect(formatted).toMatchInlineSnapshot(`"John Doe"`);
});
// 避免:冗長難以審查的快照
test("entire page render", () => {
const page = renderEntirePage();
expect(page).toMatchSnapshot(); // 可能有數千行
});
使用描述性測試名稱
// 推薦:清晰說明快照代表的內容
test("formats currency with USD symbol", () => {
expect(formatCurrency(99.99)).toMatchInlineSnapshot(`"$99.99"`);
});
// 避免:測試意圖不明確
test("format test", () => {
expect(format(99.99)).toMatchInlineSnapshot(`"$99.99"`);
});
分組相關快照
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();
});
});
處理動態資料
// 推薦:標準化動態資料
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"
組織大型快照檔案
對於大型專案,建議合理組織測試以保持快照檔案可管理:
目錄結構
tests/
├── components/
│ ├── Button.test.tsx
│ └── __snapshots__/
│ └── Button.test.tsx.snap
├── utils/
│ ├── formatters.test.ts
│ └── __snapshots__/
│ └── formatters.test.ts.snap
故障排除
快照失敗
快照失敗時,Bun 會顯示差異:
diff
{
- "name": "John",
+ "name": "Jane",
}
- Expected - 1
+ Received + 1
常見原因:
- 有意更改(使用
--update-snapshots更新) - 無意更改(修正程式碼)
- 動態資料(使用屬性匹配器)
- 環境差異(標準化資料)
平臺差異
注意平臺間的差異:
// Windows 與 Unix 路徑可能不同
test("file operations", () => {
const result = processFile("./test.txt");
expect({
...result,
path: result.path.replace(/\\/g, "/"), // 統一路徑格式
}).toMatchSnapshot();
});