zudo-test-wisdom

Type to search...

to open search from anywhere

Vitestパターン

Vitest を使ったユニットテスト、コンポーネントテスト、ビルド出力テスト。

ワークスペースレベルのVitest設定

大規模なプロジェクトでは、異なるテストタイプに異なるVitest設定が必要になることがよくあります。Vitestワークスペースで管理します:

// vitest.workspace.ts
import { defineWorkspace } from "vitest/config";

export default defineWorkspace([
  {
    test: {
      name: "unit",
      include: ["src/**/*.test.ts"],
      environment: "node",
    },
  },
  {
    test: {
      name: "component",
      include: ["src/**/*.test.tsx"],
      environment: "jsdom",
    },
  },
  {
    test: {
      name: "build",
      include: ["tests/build/**/*.test.ts"],
      environment: "node",
    },
  },
]);

💡 Tip

設定を分離することで、高速なユニットテストを低速なコンポーネントテストやビルドテストとは独立して実行できます:vitest --project unit vs vitest --project component

jsdomとhappy-dom環境

コンポーネントテストに適切なDOM環境を選択します:

// vitest.config.ts for component tests
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    environment: "jsdom",
    // Or use happy-dom for faster execution:
    // environment: "happy-dom",
    globals: true,
    setupFiles: ["./test-setup.ts"],
  },
});

必要に応じてファイル単位の環境オーバーライドも可能です:

// @vitest-environment jsdom
import { describe, it, expect } from "vitest";

describe("DOM-dependent test", () => {
  it("manipulates the document", () => {
    document.body.innerHTML = '<div id="app">Hello</div>';
    expect(document.getElementById("app")?.textContent).toBe("Hello");
  });
});

テストタイプ別の設定分離

mdx-formatterのパターン:ユニットテスト、APIテスト、関数テスト用の別々の設定:

vitest.config.ts          # Default: unit tests
vitest.config.api.ts      # API integration tests
vitest.config.functions.ts # Cloud function tests
{
  "scripts": {
    "test": "vitest",
    "test:api": "vitest --config vitest.config.api.ts",
    "test:functions": "vitest --config vitest.config.functions.ts",
    "test:all": "vitest && vitest --config vitest.config.api.ts"
  }
}

コントラクトテスト: VitestによるRustエンジン

mdx-formatterの実例:VitestスイートがRustフォーマッティングエンジンのコントラクトテストとして機能します。Node.jsラッパーがRustバイナリを呼び出し、Vitestが出力の期待値との一致を検証します:

// tests/contract.test.ts
import { describe, it, expect } from "vitest";
import { execSync } from "child_process";

describe("Rust formatter contract", () => {
  it("formats basic MDX correctly", () => {
    const input = "# Hello\nSome   text   here";
    const result = execSync(`echo '${input}' | ./target/release/formatter`, {
      encoding: "utf-8",
    });
    expect(result.trim()).toBe("# Hello\n\nSome text here");
  });
});

📝 Note

コントラクトテストにより、上位レベルの言語からバイナリの動作を検証できます。Vitestスイートが仕様として機能し、Rustエンジンの動作が変わるとコントラクトテストがキャッチします。

冪等性テスト

フォーマッターやトランスフォーマーにとって強力な不変条件:操作を2回適用した結果は、1回適用した結果と同じでなければなりません。

// tests/idempotency.test.ts
import { describe, it, expect } from "vitest";
import { format } from "../src/format";
import { readFileSync, readdirSync } from "fs";
import { join } from "path";

const FIXTURES_DIR = join(__dirname, "fixtures");

describe("idempotency", () => {
  const fixtures = readdirSync(FIXTURES_DIR).filter((f) =>
    f.endsWith(".mdx")
  );

  for (const fixture of fixtures) {
    it(`is idempotent for ${fixture}`, () => {
      const input = readFileSync(join(FIXTURES_DIR, fixture), "utf-8");
      const firstPass = format(input);
      const secondPass = format(firstPass);
      expect(firstPass).toBe(secondPass);
    });
  }
});

Miniflare + D1/R2 インテグレーションテスト

zudo-pattern-genの実例:MiniflareによるローカルD1データベースとR2ストレージを使ったCloudflare Workersのテスト:

// tests/integration.test.ts
import { describe, it, expect, beforeAll } from "vitest";
import { Miniflare } from "miniflare";

describe("Worker with D1", () => {
  let mf: Miniflare;

  beforeAll(async () => {
    mf = new Miniflare({
      modules: true,
      script: `export default { async fetch(req, env) { /* ... */ } }`,
      d1Databases: ["DB"],
      r2Buckets: ["STORAGE"],
    });

    // Run migrations
    const db = await mf.getD1Database("DB");
    await db.exec(`
      CREATE TABLE IF NOT EXISTS patterns (
        id TEXT PRIMARY KEY,
        name TEXT NOT NULL,
        data TEXT NOT NULL
      )
    `);
  });

  it("stores and retrieves a pattern", async () => {
    const resp = await mf.dispatchFetch("http://localhost/api/patterns", {
      method: "POST",
      body: JSON.stringify({ name: "test", data: "{}" }),
    });
    expect(resp.status).toBe(201);

    const getResp = await mf.dispatchFetch("http://localhost/api/patterns");
    const patterns = await getResp.json();
    expect(patterns).toHaveLength(1);
    expect(patterns[0].name).toBe("test");
  });
});

💡 Tip

Miniflareは同じWorkersランタイムをローカルで実行するため、インテグレーションテストは本番環境の動作に近くなります。D1とR2のバインディングを組み合わせることで、デプロイなしに完全なデータフローをテストできます。

Revision History