Claude Media
Claude CodeでCucumber(Gherkin)の.featureを受け入れ条件にする

Claude CodeでCucumber(Gherkin)の.featureを受け入れ条件にする

Claude Codeに.featureを先に書かせ、cucumber-jsの通過を完了条件にするBDDの回し方。Gherkinの書き方と3層の設定例、dry-runに頼れない理由を扱います。

Claude Codeに機能を頼むとき、「何ができれば完成か」を自然文の .feature ファイルで先に固定し、cucumber-js が通るまでを完了条件にする進め方があります。Gherkinで書いたシナリオはそのまま実行できる仕様で、Claude Codeが読める成否の信号になります。

ただし、この方式は順序を決めないと崩れます。実装を先に書かせると、実装に合わせてシナリオが書き換わり、通ったことに意味がなくなります。この記事では、.featureを人が確認してから固定し、実装側だけを動かす3層の設定を示します。

単体テストを先に書かせる流れはClaude Codeでテスト駆動開発(TDD)を回す手順にあります。Pesterで完了条件を作る例はClaude CodeでPowerShellのPesterテストを完了条件にするに載せています。ここでは、テストコードではなく業務の言葉で書く受け入れ条件に絞ります。

.featureが受け入れ条件として使える理由

Gherkinリファレンスは、Exampleを「仕様でありドキュメントであり、同時にテストでもある」ものと説明しています。ExampleをまとめたFeatureは、全体として実行可能な仕様になります。

Claude Codeにとっての利点は、検証の単位が業務の言葉で書いたシナリオになることです。失敗した約束と、実装の都合で書かれたテスト名が分離されます。

構造は3段に決まっています。

手順

Exampleの基本形

  1. 1

    Given

    初期状態を作ります。ユーザー操作の話は入れません。

  2. 2

    When

    イベントや操作を1つ書きます。

  3. 3

    Then

    利用者や外部システムから観察できる結果を書きます。

Thenについて、リファレンスは「データベースのレコードのような内部の挙動ではなく、レポート・画面・メッセージなど観察できる出力」を確かめるよう求めています。この規約は、CLAUDE.mdに書いて先に伝えておく価値があります。

Claudeに守らせる.featureの書き方

Gherkinの規則のうち、Claude Codeに書かせるときにつまずきを減らせるものを選びます。

  • 1つの.featureファイルに書けるFeatureは1つだけです
  • 1つのExampleは3〜5ステップが推奨で、増やすと仕様としての表現力が落ちます
  • 「1922年を想像する」という指針があり、技術やUIを前提にせず、実装の詳細はステップ定義に隠します
  • 同じ文面のステップは、Given・When・Thenのどれで書いても重複とみなされます(キーワードはステップ定義の照合で無視される)
  • Backgroundは短く保ちます。4行を超えるなら、細部を上位のステップに移すことが勧められています
  • 同じ手順を値だけ変えて繰り返すなら、Scenario OutlineとExamplesの表にまとめます

重複の規則は見落としやすい点です。「口座にお金がある」を前提でも結果でも使い回すと、同じステップ定義に解決されて意図しない検証になります。リファレンスの例のように、「口座の残高が430円である」「口座の残高が430円になっている」と文面を分けると、曖昧さが消えます。

もう1つ、書式の罠があります。キーワードの後ろにコロンを付ける必要があるものとないものがあり、不要なコロンを付けるとテストが無視されます。Feature・Rule・Example/Scenario・Background・Scenario Outline・Examplesはコロン付き、Given・When・Then・And・Butは付けません。Claude Codeの書いた.featureを見るときは、この点を最初に目で追います。

日本語の文面で書く場合、キーワードは英語のままでも、ステップの文面は日本語で構いません。キーワードごと日本語にするなら、ファイル先頭に # language: ja のようなヘッダーを置きます。使える語は npx cucumber-js --i18n-keywords ja で出力できます。

最小のプロジェクトで試す

cucumber-jsはプロジェクトの依存として入れます。グローバルインストールは動作の対象外で、同じ@cucumber/cucumberが複数入っていても壊れます。

npm install --save-dev @cucumber/cucumber
mkdir -p features/step_definitions src

.featureの例として、送料の計算を取り上げます。金額と送料の数値は説明用の仮のものです。

# features/shipping.feature
Feature: 送料の計算
 
  Rule: 合計5000円以上は送料無料
 
    Scenario Outline: 合計金額ごとの送料
      Given カートの合計が<subtotal>円である
      When 送料を計算する
      Then 送料は<fee>円になる
 
      Examples:
        | subtotal | fee |
        | 4999     | 500 |
        | 5000     | 0   |

境界値の4999と5000を表に入れているのが要点です。Claude Codeに.featureを書かせるときは、「境界の両側を1行ずつ含める」と指示しておくと、仕様の穴を人が見つけやすくなります。これはこの記事の提案で、Gherkinの規則ではありません。

ステップ定義は、cucumber-jsの既定では features/ 配下の .js / .cjs / .mjs が読み込まれます。Cucumber式の {int} で数値を受け取ります。

// features/step_definitions/shipping.steps.mjs
import assert from 'node:assert/strict';
import { Given, When, Then } from '@cucumber/cucumber';
import { shippingFee } from '../../src/shipping.mjs';
 
Given('カートの合計が{int}円である', function (subtotal) {
  this.subtotal = subtotal;
});
 
When('送料を計算する', function () {
  this.fee = shippingFee(this.subtotal);
});
 
Then('送料は{int}円になる', function (expected) {
  assert.equal(this.fee, expected);
});

ステップ定義でワールド(this)を使うときは、アロー関数にしません。アロー関数では this から参照できなくなるためです。Scenario Outlineの <subtotal> は、ステップ定義を探す前に表の値へ置き換えられます。

通過の意味を決める実行コマンド

完了条件のコマンドは npx cucumber-js です。判定は終了ステータスで行うため、一度わざとシナリオを壊して echo $? で0以外になることを確かめてから、フックに組み込みます。

設定ファイルのオプションのうち、完了条件の設計に関わるものを挙げます。

オプション既定値完了条件での扱い
strict既定値true完了条件での扱いpendingのステップがあると実行が失敗します。そのままにします
failFast既定値false完了条件での扱い最初の失敗で止めます。1件ずつ直させるループに向きます
dryRun既定値false完了条件での扱いステップを実行せず、未定義のステップも失敗にしません。完了条件には使えません
paths既定値features/**/*.feature完了条件での扱い.featureの場所を変えるときに指定します

--dry-run は、シナリオの収集と未定義ステップの報告までを行うモードです。ステップは実行されず「skipped」と報告されるので、通過の証拠にはなりません。Claude Codeが「dry-runが通りました」と報告したら、実行された検証ではなく、構文と一致の確認にとどまっている点に注意が必要です。

逆に、この性質は最初の工程で役立ちます。.featureを書かせた直後に --dry-run を回すと、ステップ定義がまだ無いシナリオを一覧でき、スニペットのフォーマッターで定義の雛形も出せます。

CLAUDE.mdで順序を固定する

CLAUDE.md は強制ではなく文脈として読まれるため、メモリのドキュメントは「検証できるほど具体的に書く」ことを勧めています。次の断片は一例です。

# 受け入れ条件の進め方
 
## 順序
1. 機能の依頼を受けたら、実装より先に features/ に.featureを書く
2. 書いたら止まり、人が内容を確認するのを待つ
3. 確認後は features/*.feature を編集しない。直したくなったら理由を伝えて止まる
 
## 完了条件
- `npx cucumber-js` を実行し、失敗が0件であることを確認してから完了を報告する
- `--dry-run` の結果を通過の根拠にしない
- Thenのステップは画面・出力・レスポンスなど観察できる結果を検証する
- 境界値は両側を1行ずつ表に含める

3つ目の「確認後は編集しない」が肝です。次の層で、この約束を設定にも落とします。

permissionsで.featureの編集を止める

実装フェーズに入ったら、.featureへの書き込みを拒否します。permissionsの Edit 規則は、gitignoreの記法でパスを指定できます。

{
  "permissions": {
    "allow": [
      "Bash(npx cucumber-js *)"
    ],
    "deny": [
      "Edit(features/**/*.feature)"
    ]
  }
}

拒否規則はClaudeの組み込みのファイルツールと、sed や tee、リダイレクトなどBashで認識されるファイルコマンドに効きます。一方で、PythonやNodeのスクリプトが自分でファイルを開いて書き換える場合には効きません。厳密に止めたいなら、OSレベルで制限できるsandboxの利用がドキュメントで案内されています。

.featureを最初に書かせるセッションでは、この拒否規則を外しておきます。フェーズごとに settings.local.json へ追加する運用にすると、書くときと固定するときの切り替えが明確になります。

Stop hookで通過まで止めさせない

CLAUDE.mdが守られなかったときの最後の砦が、Stop hookです。hooksのリファレンスでは、Stop hookが終了コード2で終わるとClaudeの停止が防がれ、会話が続きます。stderrの内容が、続行の理由としてClaudeに渡されます。

#!/bin/bash
# .claude/hooks/cucumber-gate.sh
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0
fi
if ! OUT=$(npx cucumber-js --fail-fast 2>&1); then
  echo "$OUT" | tail -n 60 >&2
  exit 2
fi

登録は settings.json で行います。

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": ".claude/hooks/cucumber-gate.sh" }
        ]
      }
    ]
  }
}

stop_hook_active は、stop hookの結果としてすでに続行している最中に true です。上のスクリプトはその場合にゲートを開けるため、修正のやり直しは1回分になります。複数回の反復を許すなら、この判定を外して連続ブロックの上限に任せる選択もあります。上限は、ツール呼び出しを挟まず連続8回で、環境変数 CLAUDE_CODE_STOP_HOOK_BLOCK_CAP で変えられます。詳しくはStop hookのブロック上限を調整する環境変数で扱っています。

--fail-fast と出力の tail を組み合わせると、Claude Codeに渡る情報は最初の失敗シナリオ周辺に絞られます。全失敗を一度に見せたいときは、--fail-fast を外します。テストの置き場所をPostToolUseにするかStopにするかは、実行時間で決めます。判断の目安はClaude Code hooksでテストを自動実行するにあります。

つまずきやすい点

無視されるシナリオ

コロンの付け間違いは、エラーではなくシナリオの無視として現れます。実行されたシナリオ数が期待と合うかを見ます。

Thenが何も確かめていない

Then のステップ定義が空、あるいは assert を呼んでいないと、必ず通ります。通過のたびにシナリオ数だけ増えて、検証は増えていません。Claude Codeに書かせたステップ定義は、各Thenに期待値との比較があるかを人が確認します。

pendingやundefinedの扱い

strict が既定のtrueなら、pendingのステップは失敗扱いです。設定で --no-strict にすると通ってしまうため、設定ファイルをClaude Codeに触らせるときは差分を見ます。

ステップ文面の重複

キーワードは照合に使われないので、GivenとThenで同じ文面を書くと同じ定義に当たります。文面は、状態を表す形と結果を表す形で変えます。

インストールの二重化

ステップ定義を別パッケージに切り出して、そこにも別の@cucumber/cucumberが依存していると、「Cucumberが動いていないインスタンスで関数を呼んでいる」という趣旨のエラーが出ます。npm why @cucumber/cucumber で依存の重複を調べます。

ハングして終わらない

cucumber-jsは既定でイベントループが空になるまで終了しません。--force-exit で強制終了できますが、CLIドキュメントは原因を直すほうを勧めています。

進め方の手順

最初の依頼は、実装ではなく仕様の確認から始めます。

  1. 依頼の文面から.featureを書かせ、境界値とRuleの切り方を人が読む
  2. npx cucumber-js --dry-run で、ステップが未定義のシナリオを一覧にする
  3. .featureの確認が済んだら、deny 規則を追加して固定する
  4. ステップ定義と実装を書かせ、npx cucumber-js の通過で完了とする
  5. Stop hookを入れ、わざと壊したシナリオで止まらず修正に進むかを確かめる

まとめ

.featureを受け入れ条件にする利点は、人が読める約束をClaude Codeが実行できる形で持てることです。弱点は、約束そのものをClaude Codeが書き換えられる点にあります。順序をCLAUDE.mdで決め、確認後の.featureをpermissionsで守り、通過までをStop hookで止めさせない3層で補えば、通ったという事実に意味が残ります。

人が読むのは、実装ではなく.featureとThenの検証内容です。そこだけを見れば、受け入れの責任は人の側に残せます。

この記事を共有:XはてブLinkedIn