執行環境套件管理器打包器測試執行器程式碼檢查格式化指南參考部落格意見回饋

快照測試

學習如何在 Bun 中使用快照測試,在測試執行之間儲存並比較輸出

快照測試會儲存某個值的輸出,並將其與未來測試執行中的輸出進行比較。它適用於 UI 元件、複雜物件,或任何需要保持一致的輸出。

基本快照

使用 .toMatchSnapshot() 匹配器編寫快照測試:

test.ts
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 會將內聯快照直接儲存在測試檔案中:

test.ts
import { test, expect } from "bun:test";

test("inline snapshot", () => {
  // 首次執行:快照將自動插入
  expect({ hello: "world" }).toMatchInlineSnapshot();
});

首次執行後,Bun 會自動更新你的測試檔案:

test.ts
import { test, expect } from "bun:test";

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

使用內聯快照步驟

  1. 使用 .toMatchInlineSnapshot() 編寫測試
  2. 執行一次測試
  3. Bun 會自動使用快照更新你的測試檔案
  4. 後續執行環境,Bun 會將該值與內聯快照進行比較

錯誤快照

你還可以使用 .toThrowErrorMatchingSnapshot() 和 .toThrowErrorMatchingInlineSnapshot() 對錯誤訊息進行快照:

test.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();
});

執行後,內聯版本變為:

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

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

高階快照用法

複雜物件

快照適用於複雜的巢狀物件:

test.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();
});

陣列快照

陣列同樣適合快照測試:

test.ts
import { test, expect } from "bun:test";

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

函式輸出快照

快照函式的傳回值:

test.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 元件:

test.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),使用屬性匹配器:

test.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),
  });
});

快照檔案儲存:

快照檔案
exports[`snapshot with dynamic values 1`] = `
{
  "createdAt": Any<String>,
  "id": Any<Number>,
  "name": "John",
}
`;

最佳實踐

保持快照檔案簡潔

test.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(); // 可能有數千行
});

使用描述性測試名稱

test.ts
// 推薦:清晰說明快照代表的內容
test("formats currency with USD symbol", () => {
  expect(formatCurrency(99.99)).toMatchInlineSnapshot(`"$99.99"`);
});

// 避免:測試意圖不明確
test("format test", () => {
  expect(format(99.99)).toMatchInlineSnapshot(`"$99.99"`);
});

分組相關快照

test.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();
  });
});

處理動態資料

test.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"

組織大型快照檔案

對於大型專案,建議合理組織測試以保持快照檔案可管理:

目錄結構
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 更新)
  • 無意更改(修正程式碼)
  • 動態資料(使用屬性匹配器)
  • 環境差異(標準化資料)

平臺差異

注意平臺間的差異:

test.ts
// Windows 與 Unix 路徑可能不同
test("file operations", () => {
  const result = processFile("./test.txt");

  expect({
    ...result,
    path: result.path.replace(/\\/g, "/"), // 統一路徑格式
  }).toMatchSnapshot();
});