gwt-tester について
gwt-testerは、Given/When/Thenの意味構造で読みやすいテストを書くためのスキルです。言語に依存しないルールと手順を示し、JavaScript/TypeScriptとDartのテスト慣習を例にしています。
主な機能
- 01Given/When/Thenに沿ったテスト構成
- 02ネスト深度とブロックスコープのルール
- 03準備、アクション、アサーションの分離
- 04Whenブロック内でのバリエーション網羅
- 05再利用範囲に応じたテストユーティリティ配置
- 06チェックリストによるテストの確認とリファクタリング
活用シーン
- 01テストをGiven/When/Thenシナリオに再構成
- 02準備、アクション、アサーションの明確な分離
- 03同じ動作の複数インターフェースやバリエーションの網羅
- 04テストユーティリティと共有値の配置判断
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ hexsis-llc/ skills --skill gwt-testerインストール後、Claude Code で/gwt-testerを入力して読み込まれたか確認します。
詳細
GWT テスター
Given/When/Then のセマンティクスに沿った、読みやすく明快なテストを書くために、以下の指示に従ってください。
用語マップ
このスキル全体で、以下の抽象用語を使用してください。
| 抽象用語 | JS/TS の例 | Dart の例 |
|---|---|---|
| スイートブロック | describe | group |
| ケースブロック | it / test | test |
| 一度だけのセットアップ(スイート用) | before | setUpAll |
| 各ケースのセットアップ | beforeEach | setUp |
| 一度だけのティアダウン(スイート用) | after | tearDownAll |
| 各ケースのティアダウン | afterEach | tearDown |
このスキルで次の用語が使われている場合:
- 「スイートブロック」は
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)を指します
ルール
- ネストの最大深度は3階層にします:
suite -> suite -> case。 例外:同じ動作をインターフェースやバリアント(例:via RPCとvia REST)ごとに検証するWhenバリアントの網羅では、suite -> suite -> suite -> caseを許可します。 この例外では、バリアントはレベル3に配置し、Thenケースブロックはレベル4に配置します。 Givenブロックを別のGivenブロックの中にネストしてはいけません。Givenのセットアップフックでは、テスト対象の主な動作ではなく、共有コンテキストとテストデータに焦点を当てます。- アサーションのみを行うケースブロックを優先します。
- 可能な限り、実行する動作は専用の
Whenブロックに移します。そのアクションをケースごとに一度だけ実行するため、Whenブロック内でセットアップフックを使用します。 シナリオで実行可能なアクションがない場合は、Whenブロックを省略し、ThenケースをGivenブロックの直下に配置します。Whenにバリアントが含まれる場合は、親のWhenブロックではなく、各バリアントブロック内にアクションのセットアップフックを配置します。 - ブロックのタイトルは、そのスコープで実際に行われる動作を表すようにします。別のブロックのセットアップフックですでに
<action>が実行されている場合、ケースにWhen <action> ...というタイトルを付けてはいけません。 WhenにThenが1つだけ含まれる場合は、単一のケースタイトル(When ... then ...)を使用します。このようにまとめる場合は、ケース本体内にWhenのアクションを記述してもかまいません。- シナリオの文脈を追加しないラッパースイートブロックは削除します。
- シナリオのタイトルは具体的に記述します:
Given <subject> with <condition>、任意でWhen <action>、そしてThen <outcome>。 - スコープに基づいてテストユーティリティを配置します:
- あるテストファイルだけで使うユーティリティは、そのファイルの末尾に配置し、テストブロックがファイルの先頭に来るようにします。
- 複数のテストファイルで再利用されるユーティリティは、既存のコードベースの規約に従って、共有テストユーティリティに移動します。
- 共有ユーティリティの規約が存在しない場合は、適切な
utilsディレクトリを作成し、共有ユーティリティをそこに配置します。
- 値を名前付き変数に取り出すのは、その値を複数回使用する場合に限り、テストデータを予測可能かつ一元的に更新できるよう、最も近い共有スコープ(例:
GivenまたはWhen)に配置します。
テスト作成ワークフロー
- シナリオをトップレベルの
Given ...スイートブロックとして定義します。 - 共有コンテキストの設定に限り、
Givenのセットアップフックを追加します。 - 各アクションの経路に対して、ネストした
When ...スイートブロックを追加します。 実行するアクションがない場合はWhenを省略し、ThenケースをGivenの直下に配置します。 - 複数の
Thenケースでアクションを共有する場合は、Whenブロック内のセットアップフックでアクションを実行します。 そのWhenの下にバリエーションがある場合は、各バリエーションブロック内にセットアップフックを配置します。 - アサーションのみを含む
Then ...ケースブロックを追加します。 WhenにThenが 1 つしかない場合は、任意でタイトルを 1 つのケースタイトルWhen ... then ...にまとめます。- アクション固有のロジックは、
GivenフックからWhenフックに移します。 Whenでインターフェースやバリエーションを網羅する必要がある場合は、そのWhenの下にのみバリエーションブロックをネストします(例:via RPC、via REST)。- ネストは浅く保ち、冗長なラッパーを削除します。
- テストとフォーマッターを実行します。
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)
})
})
})
リファクタリングガイド
テストのリファクタリングや既存テストの確認を依頼された場合は、編集のきっかけとなったルールだけでなく、すべてのルールに照らして評価します。
次の順序で確認します。
- 現在の構造を
Given、When、Thenの各ブロックとして整理します。 - アクション固有のロジックを
Givenのセットアップフックから適切なWhenブロックに移します。 アクションのないシナリオは、空のWhenを追加せず、Givenの直下に置きます。 - ロジックを移すたびに、統合できるかをすぐに再評価します。
WhenブロックにThenが 1 つだけ残った場合は、When ... then ...にまとめます。- ほかにも結果がある場合は、
Whenと複数のThenケースを維持します。
- 構造を編集した後に、タイトルとスコープの整合性を再確認します。
- 完了前にレビューのチェックリスト全体を実行します。
アンチパターン
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)?