mcpField
G
gwt-tester

gwt-tester

github.com/hexsis-llcテスト・品質1110 回インストール2 日前に更新
リポジトリ

gwt-tester について

gwt-testerは、Given/When/Thenの意味構造で読みやすいテストを書くためのスキルです。言語に依存しないルールと手順を示し、JavaScript/TypeScriptとDartのテスト慣習を例にしています。

主な機能

  1. 01Given/When/Thenに沿ったテスト構成
  2. 02ネスト深度とブロックスコープのルール
  3. 03準備、アクション、アサーションの分離
  4. 04Whenブロック内でのバリエーション網羅
  5. 05再利用範囲に応じたテストユーティリティ配置
  6. 06チェックリストによるテストの確認とリファクタリング

活用シーン

  1. 01テストをGiven/When/Thenシナリオに再構成
  2. 02準備、アクション、アサーションの明確な分離
  3. 03同じ動作の複数インターフェースやバリエーションの網羅
  4. 04テストユーティリティと共有値の配置判断

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/hexsis-llc/skills --skill gwt-tester

インストール後、Claude Code で/gwt-testerを入力して読み込まれたか確認します。

詳細

GWT テスター

Given/When/Then のセマンティクスに沿った、読みやすく明快なテストを書くために、以下の指示に従ってください。

用語マップ

このスキル全体で、以下の抽象用語を使用してください。

抽象用語JS/TS の例Dart の例
スイートブロックdescribegroup
ケースブロックit / testtest
一度だけのセットアップ(スイート用)beforesetUpAll
各ケースのセットアップbeforeEachsetUp
一度だけのティアダウン(スイート用)aftertearDownAll
各ケースのティアダウンafterEachtearDown

このスキルで次の用語が使われている場合:

  • 「スイートブロック」は describe(JS/TS)または group(Dart)を指します
  • 「ケースブロック」は it/test(JS/TS)または test(Dart)を指します
  • 「セットアップフック」は before/beforeEach(JS/TS)または setUpAll/setUp(Dart)を指します
  • 「ティアダウンフック」は after/afterEach(JS/TS)または tearDownAll/tearDown(Dart)を指します

ルール

  1. ネストの最大深度は3階層にします:suite -> suite -> case。 例外:同じ動作をインターフェースやバリアント(例:via RPC と via REST)ごとに検証する When バリアントの網羅では、suite -> suite -> suite -> case を許可します。 この例外では、バリアントはレベル3に配置し、Then ケースブロックはレベル4に配置します。
  2. Given ブロックを別の Given ブロックの中にネストしてはいけません。
  3. Given のセットアップフックでは、テスト対象の主な動作ではなく、共有コンテキストとテストデータに焦点を当てます。
  4. アサーションのみを行うケースブロックを優先します。
  5. 可能な限り、実行する動作は専用の When ブロックに移します。そのアクションをケースごとに一度だけ実行するため、When ブロック内でセットアップフックを使用します。 シナリオで実行可能なアクションがない場合は、When ブロックを省略し、Then ケースを Given ブロックの直下に配置します。 When にバリアントが含まれる場合は、親の When ブロックではなく、各バリアントブロック内にアクションのセットアップフックを配置します。
  6. ブロックのタイトルは、そのスコープで実際に行われる動作を表すようにします。別のブロックのセットアップフックですでに <action> が実行されている場合、ケースに When <action> ... というタイトルを付けてはいけません。
  7. When に Then が1つだけ含まれる場合は、単一のケースタイトル(When ... then ...)を使用します。このようにまとめる場合は、ケース本体内に When のアクションを記述してもかまいません。
  8. シナリオの文脈を追加しないラッパースイートブロックは削除します。
  9. シナリオのタイトルは具体的に記述します:Given <subject> with <condition>、任意で When <action>、そして Then <outcome>。
  10. スコープに基づいてテストユーティリティを配置します:
    • あるテストファイルだけで使うユーティリティは、そのファイルの末尾に配置し、テストブロックがファイルの先頭に来るようにします。
    • 複数のテストファイルで再利用されるユーティリティは、既存のコードベースの規約に従って、共有テストユーティリティに移動します。
    • 共有ユーティリティの規約が存在しない場合は、適切な utils ディレクトリを作成し、共有ユーティリティをそこに配置します。
  11. 値を名前付き変数に取り出すのは、その値を複数回使用する場合に限り、テストデータを予測可能かつ一元的に更新できるよう、最も近い共有スコープ(例:Given または When)に配置します。

テスト作成ワークフロー

  1. シナリオをトップレベルの Given ... スイートブロックとして定義します。
  2. 共有コンテキストの設定に限り、Given のセットアップフックを追加します。
  3. 各アクションの経路に対して、ネストした When ... スイートブロックを追加します。 実行するアクションがない場合は When を省略し、Then ケースを Given の直下に配置します。
  4. 複数の Then ケースでアクションを共有する場合は、When ブロック内のセットアップフックでアクションを実行します。 その When の下にバリエーションがある場合は、各バリエーションブロック内にセットアップフックを配置します。
  5. アサーションのみを含む Then ... ケースブロックを追加します。
  6. When に Then が 1 つしかない場合は、任意でタイトルを 1 つのケースタイトル When ... then ... にまとめます。
  7. アクション固有のロジックは、Given フックから When フックに移します。
  8. When でインターフェースやバリエーションを網羅する必要がある場合は、その When の下にのみバリエーションブロックをネストします(例:via RPC、via REST)。
  9. ネストは浅く保ち、冗長なラッパーを削除します。
  10. テストとフォーマッターを実行します。

When のバリエーション網羅

同じ When アクションの下でバリエーションを網羅する場合に限り、ネストを 1 段階追加します。 アクションのセットアップフックは、親の When ブロックではなく、各バリエーションブロック内に配置します。

例 1(バリエーション内のセットアップ):

suite("Given an endpoint that returns a user", () => {
  suite("When requesting user 42", () => {
    suite("via RPC", () => {
      setup_each(() => {
        response = requestUserViaRpc(42)
      })

      case("Then status is successful", () => {
        assert(response.status == 200)
      })
    })

    suite("via REST", () => {
      setup_each(() => {
        response = requestUserViaRest(42)
      })

      case("Then status is successful", () => {
        assert(response.status == 200)
      })
    })
  })
})

例 2(バリエーションごとに複数の Then の結果):

suite("Given a user endpoint", () => {
  suite("When requesting user 42", () => {
    suite("via RPC", () => {
      setup_each(() => {
        response = requestUserViaRpc(42)
      })

      case("Then status is successful", () => {
        assert(response.status == 200)
      })

      case("Then user payload is present", () => {
        assert(response.body.user != null)
      })
    })

    suite("via REST", () => {
      setup_each(() => {
        response = requestUserViaRest(42)
      })

      case("Then status is successful", () => {
        assert(response.status == 200)
      })

      case("Then user payload is present", () => {
        assert(response.body.user != null)
      })
    })
  })
})

疑似コードのパターン

suite("Given <subject> with <state>", () => {
  setup_each(() => {
    // arrange shared context
  })

  suite("When <action>", () => {
    setup_each(() => {
      // act
    })

    case("Then <outcome A>", () => {
      // assert only
    })

    case("Then <outcome B>", () => {
      // assert only
    })
  })
})

アクションのないシナリオ(When を省略):

suite("Given <subject> with <state>", () => {
  setup_each(() => {
    // arrange shared context only
  })

  case("Then <outcome A>", () => {
    // assert only
  })
})

タイトルとスコープの整合性

動作が発生する場所に合ったタイトルを使用します。

悪い例:

suite("Given a counter at zero", () => {
  setup_each(() => {
    // Bad: action belongs in a When block, not Given setup.
    counter.increment()
  })

  case("Then value is one", () => {
    assert(counter.value == 1)
  })
})

良い例:

suite("Given a counter at zero", () => {
  setup_each(() => {
    // arrange
  })

  suite("When incremented", () => {
    setup_each(() => {
      counter.increment()
    })

    case("Then value is one", () => {
      assert(counter.value == 1)
    })

    case("Then value is not zero", () => {
      assert(counter.value != 0)
    })
  })
})

ブロックを整理するためのロジックの移動

動作を担う最も狭い範囲のブロックにロジックを移します。

  • データの作成と共有フィクスチャは、Given のセットアップフックに保持します。
  • アクション固有の呼び出しは、Given のセットアップフックから該当する When ブロックに移します。
  • Then ケースブロックにはアサーションのみを含めます。
  • 1 つの When ... then ... にまとめたケースでは、ケース本体でのアクション実行のみを許可します。

When/Then の統合

1 つのアクションに結果のアサーションが 1 つだけある場合は、統合します。

悪い例(統合していない):

suite("Given a counter at zero", () => {
  setup_each(() => {
    // arrange
  })

  suite("When incremented", () => {
    setup_each(() => {
      counter.increment()
    })

    case("Then value is one", () => {
      assert(counter.value == 1)
    })
  })
})

良い例(統合済み):

suite("Given a counter at zero", () => {
  setup_each(() => {
    // arrange
  })

  case("When incremented then value is one", () => {
    counter.increment()

    assert(counter.value == 1)
  })
})

ユーティリティの配置

再利用の範囲に応じて、ユーティリティの配置場所を選びます。

  • ファイル固有のユーティリティは、同じテストファイル内のすべてのテストブロックの後に配置します。
  • 複数ファイルで使うユーティリティは、コードベースで共有されているテストユーティリティの配置場所に移します。
  • リポジトリに共有テストユーティリティの配置場所が定められていない場合は、共有テストヘルパー用に適切な utils ディレクトリを作成します。

再利用する値

同じ値を複数回使用する場合に限り、テストのセットアップ近くで名前付き変数に切り出します。

悪い例(変数に切り出していない):

suite("Given a user lookup", () => {
  suite("When searching by user id", () => {
    case("Then it returns the matching user", () => {
      result = findUser("user-42")
      assert(result.id == "user-42")
    })

    case("Then it records an audit message for that user", () => {
      assert(formatAuditMessage("user-42") == "audit:user-42")
    })
  })
})

悪い例(スコープが広すぎる):

suite("Given a user lookup", () => {
  userId = "user-42" // Too broad: only shared inside the When block below.

  suite("When searching by user id", () => {
    case("Then it returns the matching user", () => {
      result = findUser(userId)
      assert(result.id == userId)
    })

    case("Then it records an audit message for that user", () => {
      assert(formatAuditMessage(userId) == "audit:" + userId)
    })
  })
})

良い例:

suite("Given a user lookup", () => {
  suite("When searching by user id", () => {
    userId = "user-42" // Nearest shared scope.

    case("Then it returns the matching user", () => {
      result = findUser(userId)
      assert(result.id == userId)
    })

    case("Then it records an audit message for that user", () => {
      assert(formatAuditMessage(userId) == "audit:" + userId)
    })
  })
})

リファクタリングガイド

テストのリファクタリングや既存テストの確認を依頼された場合は、編集のきっかけとなったルールだけでなく、すべてのルールに照らして評価します。

次の順序で確認します。

  1. 現在の構造を Given、When、Then の各ブロックとして整理します。
  2. アクション固有のロジックを Given のセットアップフックから適切な When ブロックに移します。 アクションのないシナリオは、空の When を追加せず、Given の直下に置きます。
  3. ロジックを移すたびに、統合できるかをすぐに再評価します。
    • When ブロックに Then が 1 つだけ残った場合は、When ... then ... にまとめます。
    • ほかにも結果がある場合は、When と複数の Then ケースを維持します。
  4. 構造を編集した後に、タイトルとスコープの整合性を再確認します。
  5. 完了前にレビューのチェックリスト全体を実行します。

アンチパターン

  • Given の中に Given をネストする。
  • When ブロックに移動できるアクションの実行を、Given のセットアップに残す。
  • When のセットアップフックで実行できる場合に、ケースブロック内で SUT を呼び出す。
  • 1 つのケースブロック内でセットアップ/アクション/アサーションを混在させる。
  • 新しいコンテキストを追加しないラッパースイートブロックを追加する。
  • 実行するアクションがないのに、空の When ブロックを追加する。
  • When のバリエーションがある場合に、Then の結果が 1 つだけでも When ... then ... にまとめる。
  • When のバリエーションがある場合の suite -> suite -> suite -> case を除き、ネストの深さを suite -> suite -> case より深くする。
  • アクションが実際には別のブロックで実行されるのに、あるブロック内で実行されると主張するタイトルを付ける。

レビューチェックリスト

  • リファクタリングのきっかけとなったルールだけでなく、すべてのルールを確認しましたか?
  • ネストされた Given ブロックはありませんか?
  • 各ケースブロックにはアサーションのみが含まれていますか?
  • Given のセットアップには共有コンテキストのみが含まれていますか?
  • アクションのロジックは該当する When ブロックに移動されていますか?
  • 実行するアクションがない場合、When を省略し、Then ケースを Given の直下に配置していますか?
  • When のバリエーションがある場合、アクションのセットアップフックは親の When ブロックではなく、各バリエーションブロック内に配置されていますか?
  • ロジックを移動するたびに、When/Then をまとめるかどうかを再検討しましたか?
  • スイートとケースのタイトルは、各ブロックのスコープ内で実際に行われることと一致していますか?
  • When が 1 つ、Then が 1 つのケースは、1 つのケースブロックにまとめられていますか?
  • ファイル固有のユーティリティはテストファイルの末尾に配置されていますか(テストが先)?
  • 共有ユーティリティは、既存のコードベースのパターンに従った共有の場所(パターンがない場合は適切な新しい utils ディレクトリ)に移動されていますか?
  • 値を名前付き変数に取り出すのは複数回再利用する場合のみとし、最も近い共有スコープ(たとえば Given または When)に配置していますか?
  • ネストの深さは 3 以下ですか?または、When のバリエーションがある場合に限り suite -> suite -> suite -> case となっていますか(レベル 3)。その場合、Then ケースブロックはレベル 4 に配置されていますか(たとえば via RPC/via REST)?

コメント