Skip to content

Metanaka Vitest Rule

このリポジトリで unit test を書く・直す・レビューするときのルール。

対象は app/features/**/*.test.tsapp/shared/**/*.test.ts の Vitest テスト。

基本方針

  • テストタイトルは日本語で書く。
  • it(...) のタイトルには、前提または操作と、期待値の両方を入れる。
  • 形式は 〜した場合は〜となること、または 〜のとき〜すること を優先する。
  • タイトルだけ読んで「何をしたら、何が保証されるのか」が分かるようにする。
  • 1 つの it は 1 つの振る舞いを検証する。タイトルが長くなりすぎる場合は、テストを分ける。
  • 実装手順ではなく、ユーザーや呼び出し元から見た振る舞いを書く。

タイトルの書き方

良い例:

ts
it("有効なルーム設定フォームを送信した場合は API に入力値を渡して成功 result を返すこと", async () => {
  // ...
})

it("フォームが不正な場合は validation result を返してフィールドエラーを設定すること", async () => {
  // ...
})

it("サーバーエラー後に別の有効なフォームを送信した場合はサーバーエラーキーをクリアすること", async () => {
  // ...
})

避ける例:

ts
it("posts a valid room setup form", async () => {
  // 前提は分かるが、期待値が弱い
})

it("有効なルーム設定フォームを送信する", async () => {
  // 操作だけで、何が保証されるか分からない
})

it("server error keys", async () => {
  // 前提も期待値も曖昧
})

テスト構造

テスト本文は原則として次の順に置く。

  1. 前提を作る。
  2. 対象の操作を実行する。
  3. 期待値を検証する。

明示的な // Arrange// Act// Assert コメントは、読みやすさが上がる場合だけ使う。短いテストではコメントより空行で区切る。

小さな変更 PR の review では、今回追加・変更された test を優先して現行ルールへ寄せる。 PR の目的から外れる無関係な既存 test の title や構造違反は、一括修正を求めない。

期待値

  • タイトルに書いた期待値は、必ず assertion で直接確認する。
  • 回帰テストでは「壊したくない状態変化」をタイトルに含める。
  • snapshot だけでは title の期待値が直接確認できない場合がある。重要な contract は toEqualtoStrictEqual、個別 property の assertion などで直接確認する。
  • async の途中状態を検証する場合は、未解決の Promise を放置しない。resolve 関数を握って最後に解決し、対象 Promise を await する。
  • expect(...).toEqual(...) は、重要な contract が読み取れるように必要十分な粒度で書く。

モックと helper

  • 外部境界や依存注入された API は vi.fn() でよい。
  • 外部境界が分離されていないファイルをテストする場合は、先に小さな境界へ切り出せるか検討する。HTTP、Firestore、Firebase Auth、timer、random、現在時刻、ブラウザ API などは、feature composable や adapter の引数、または shared の薄い wrapper に寄せてから mock する。
  • 境界を切るときは、業務ロジックから見た入力と出力が分かる最小の interface にする。テストのためだけに大きな service object や汎用 repository を作らない。
  • 既存コードの責務がまだ整理されておらず、今回の変更範囲で分離すると影響が大きすぎる場合は、module mock を最小限に使ってよい。その場合も、新規コードでは境界を注入できる形へ寄せる。
  • 同じ成功レスポンスや deferred Promise を複数テストで使う場合は、小さな helper に切り出してよい。
  • helper 名も期待する意図が分かる名前にする。

例:

ts
const createSuccessfulRoomResponse = () => ({
  ok: true,
  data: {
    roomId: "1b0d6f59-2d62-42f9-920d-0e0f2f7a2c41",
    roomCode: "123456",
  },
})