スナップショットテストは、値の出力を保存し、将来のテスト実行と比較します。これは、UI コンポーネント、複雑なオブジェクト、または一貫性を保つ必要がある出力に特に役立ちます。
基本的なスナップショット
スナップショットテストは、.toMatchSnapshot() マッチャーを使用して記述されます。
import { test, expect } from "bun:test";
test("snap", () => {
expect("foo").toMatchSnapshot();
});このテストが初めて実行されると、expect への引数がシリアライズされ、テストファイルの隣にある __snapshots__ ディレクトリ内の特別なスナップショットファイルに書き込まれます。
スナップショットファイル
上記のテストを実行した後、Bun は次を作成します。
your-project/
├── snap.test.ts
└── __snapshots__/
└── snap.test.ts.snapスナップショットファイルには次が含まれます。
// Bun Snapshot v1, https://bun.com/docs/test/snapshots
exports[`snap 1`] = `"foo"`;将来の実行では、引数はディスク上のスナップショットと比較されます。
スナップショットの更新
スナップショットは、次のコマンドで再生成できます。
bun test --update-snapshotsこれは以下の場合に役立ちます。
- 出力を意図的に変更した場合
- 新しいスナップショットテストを追加している場合
- 期待される出力が正当に変更された場合
インラインスナップショット
より小さな値の場合、.toMatchInlineSnapshot() を使用してインラインスナップショットを使用できます。これらのスナップショットは、テストファイルに直接保存されます。
import { test, expect } from "bun:test";
test("インラインスナップショット", () => {
// 最初の実行:スナップショットが自動的に挿入されます
expect({ hello: "world" }).toMatchInlineSnapshot();
});最初の実行後、Bun はテストファイルを自動的に更新します。
import { test, expect } from "bun:test";
test("インラインスナップショット", () => {
expect({ hello: "world" }).toMatchInlineSnapshot(`
{
"hello": "world",
}
`);
});インラインスナップショットの使用
.toMatchInlineSnapshot()を使用してテストを記述する- テストを 1 回実行する
- Bun はスナップショットを挿入してテストファイルを自動的に更新する
- 後続の実行では、値はインラインスナップショットと比較されます
インラインスナップショットは、期待される出力をテストファイル内で直接確認できるため、小さく単純な値に特に役立ちます。
エラースナップショット
.toThrowErrorMatchingSnapshot() および .toThrowErrorMatchingInlineSnapshot() を使用して、エラーメッセージもスナップショットできます。
import { test, expect } from "bun:test";
test("エラースナップショット", () => {
expect(() => {
throw new Error("何かがうまくいきませんでした");
}).toThrowErrorMatchingSnapshot();
expect(() => {
throw new Error("別のエラー");
}).toThrowErrorMatchingInlineSnapshot();
});実行後、インラインバージョンは次のようになります。
test("エラースナップショット", () => {
expect(() => {
throw new Error("何かがうまくいきませんでした");
}).toThrowErrorMatchingSnapshot();
expect(() => {
throw new Error("別のエラー");
}).toThrowErrorMatchingInlineSnapshot(`"別のエラー"`);
});高度なスナップショットの使用
複雑なオブジェクト
スナップショットは、複雑なネストされたオブジェクトでよく機能します。
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();
});配列スナップショット
配列もスナップショットテストに適しています。
import { test, expect } from "bun:test";
test("配列スナップショット", () => {
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("レポート生成", () => {
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 コンポーネントスナップショット", () => {
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("動的値を持つスナップショット", () => {
const user = {
id: Math.random(), // 毎回変更
name: "John",
createdAt: new Date().toISOString(), // これも変更
};
expect(user).toMatchSnapshot({
id: expect.any(Number),
createdAt: expect.any(String),
});
});スナップショットは次を保存します。
exports[`動的値を持つスナップショット 1`] = `
{
"createdAt": Any<String>,
"id": Any<Number>,
"name": "John",
}
`;カスタムシリアライザー
スナップショット内のオブジェクトのシリアライズ方法をカスタマイズできます。
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();
});ベストプラクティス
スナップショットを小さく保つ
// 良い:焦点を絞ったスナップショット
test("ユーザー名のフォーマット", () => {
const formatted = formatUserName("john", "doe");
expect(formatted).toMatchInlineSnapshot(`"John Doe"`);
});
// 避ける:レビューが困難な巨大なスナップショット
test("ページ全体のレンダリング", () => {
const page = renderEntirePage();
expect(page).toMatchSnapshot(); // これは数千行になる可能性があります
});説明的なテスト名を使用する
// 良い:スナップショットが何を表しているかが明確
test("通貨を USD 記号でフォーマット", () => {
expect(formatCurrency(99.99)).toMatchInlineSnapshot(`"$99.99"`);
});
// 避ける:テスト内容が不明確
test("フォーマットテスト", () => {
expect(format(99.99)).toMatchInlineSnapshot(`"$99.99"`);
});関連するスナップショットをグループ化する
import { describe, test, expect } from "bun:test";
describe("Button コンポーネント", () => {
test("primary バリアント", () => {
expect(render(<Button variant="primary">Click</Button>))
.toMatchSnapshot();
});
test("secondary バリアント", () => {
expect(render(<Button variant="secondary">Cancel</Button>))
.toMatchSnapshot();
});
test("無効状態", () => {
expect(render(<Button disabled>Disabled</Button>))
.toMatchSnapshot();
});
});動的データを処理する
// 良い:動的データを正規化
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),
});
});スナップショットの管理
スナップショットの変更をレビューする
スナップショットが変更された場合は、注意深くレビューします。
# 変更内容を確認
git diff __snapshots__/
# 変更が意図的な場合は更新
bun test --update-snapshots
# 更新されたスナップショットをコミット
git add __snapshots__/
git commit -m "UI 変更後のスナップショットを更新"未使用のスナップショットをクリーンアップする
Bun は未使用のスナップショットについて警告します。
Warning: 1 unused snapshot found:
my-test.test.ts.snap: "old test that no longer exists 1"スナップショットファイルから削除するか、利用可能な場合はクリーンアップフラグを使用してテストを実行することで、未使用のスナップショットを削除します。
大規模なスナップショットファイルの整理
大規模なプロジェクトでは、スナップショットファイルを手頃なサイズに保つためにテストを整理することを検討してください。
tests/
├── components/
│ ├── Button.test.tsx
│ └── __snapshots__/
│ └── Button.test.tsx.snap
├── utils/
│ ├── formatters.test.ts
│ └── __snapshots__/
│ └── formatters.test.ts.snapトラブルシューティング
スナップショットの失敗
スナップショットが失敗すると、差分が表示されます。
- Expected
+ Received
Object {
- "name": "John",
+ "name": "Jane",
}一般的な原因。
- 意図的な変更(
--update-snapshotsで更新) - 意図しない変更(コードを修正)
- 動的データ(プロパティマッチャーを使用)
- 環境の違い(データを正規化)
プラットフォームの違い
プラットフォーム固有の違いに注意してください。
// パスは Windows/Unix で異なる可能性があります
test("ファイル操作", () => {
const result = processFile("./test.txt");
expect({
...result,
path: result.path.replace(/\\/g, "/"), // パスを正規化
}).toMatchSnapshot();
});