ここ数ヶ月、CursorとClaude Codeを使って開発を続けており、LLMアシスタントがどこまで可能かの限界を押し広げてきました。その過程で、多くの開発者が直面する共通の問題に遭遇しました。
- 開発速度は速いが、品質にばらつきがある。うまくいくときは驚くほど良いが、失敗するときも同様に驚かされる
- 要件が不明確な場合、LLMが独自に詳細を補完するが、それが必ずしも意図したものではない
- LLMがあまりにも速く多くのコードを生成するため、開発者の認知負荷が高まり、内容を確認する際にすべてを受け入れたくなる衝動に駆られる
様々な試行錯誤の結果、ソフトウェア開発者として、LLMとの適切な協働方法を見出しました。それは受け入れテストに立ち返ることです。長期間のAI協働作業を経て、LLMとの協働は実際の人間のエンジニアとの協働と多くの共通点があることがわかりました。要件が明確であればあるほど、議論を重ねるほど、期待に沿った成果物が得られます。
要件を明確にする方法を考えたとき、キャリア初期に学んだCucumberフレームワークとGherkin構文を思い出しました。Cucumberは振る舞い駆動開発(BDD)ツールで、人間と機械の両方が読める文書を受け入れ条件として記述します。例えば、Todoアプリケーションを開発する場合、仕様の一つとしてEnterキーを押して項目を送信する機能があります。Gherkin構文を使えば、次のように記述できます。
  Scenario: Add todo item
    When I enter "Buy milk" in the input field
    And I press the Enter key
    Then I should see "Buy milk" in the list
    And the input field should be clearedしかし、これをどのように実行可能なテストに変換するのでしょうか。通常、仕様とテストロジックを橋渡しするグルーコードを記述する必要があります。
const { Given, When, Then } = require('@cucumber/cucumber');
const { expect } = require('@playwright/test');
 
// ブラウザを操作するためのページオブジェクトがあると仮定
let page;
 
When('I enter {string} in the input field', async function (text) {
  // 入力フィールドを見つけてテキストを入力
  const inputField = await page.locator('input[type="text"]');
  await inputField.fill(text);
});
 
When('I press the Enter key', async function () {
  // 入力フィールドでEnterキーを押す
  const inputField = await page.locator('input[type="text"]');
  await inputField.press('Enter');
});
 
Then('I should see {string} in the list', async function (expectedText) {
  // Todo項目がリストに表示されることを確認
  const todoItems = await page.locator('.todo-item');
  const itemTexts = await todoItems.allTextContents();
  expect(itemTexts).toContain(expectedText);
});
 
Then('the input field should be cleared', async function () {
  // 入力フィールドがクリアされたことを確認
  const inputField = await page.locator('input[type="text"]');
  const value = await inputField.inputValue();
  expect(value).toBe('');
});以前、いくつかのサイドプロジェクトでCucumberを使用したことがありますが、本番環境のプロジェクトでは一度も使ったことがありません。主な理由は、このような仕組みの導入が容易ではないからです。チームがTDDを受け入れることさえ珍しいのに、仕様から自動テストへの橋渡しをするのは言うまでもありません。
また、私が主にスタートアップチームで働いていたことも関係しています。スタートアップでは通常、仕様からテストまでのサイクル計画を実践する時間的余裕がありません。
しかし、最大の障壁はグルーコードの記述でした。各文を個別のアクションに分解して記述するため、一つのテストシナリオが多くの小さな断片に分割されます。また、Gherkinを記述する際も注意が必要で、同じ機能の文は同じように書かないと、グルーコード内で統合できません。例えば、
When I click the button "ok"
When I go to click the button "ok"これらは二つの異なるテストロジックの断片に分割されてしまいます。同じことをする際は、記述を完全に一致させる必要があります。
要するに、Cucumberの使用は新鮮で興味深い体験でしたが、様々な障壁により本番プロジェクトで使用したことはありませんでした。
しかし、LLMがソフトウェア開発に参入した時代では状況が変わりました。LLMはGherkinで記述された仕様を直接読み取り、グルーコードなしで直接実行できるからです。
LLMは仕様を直接読み取って理解でき、Model Context Protocol(MCP)を通じてCursorやClaude Codeがブラウザやモバイルエミュレータを操作して開発を支援できます。つまり、Gherkinで期待される動作を記述すれば、LLMはMCPを通じて開発成果が受け入れ基準を満たしているかを自己確認できるのです。
Gherkin構文は優れた橋渡し役となります。これは人間とLLMの両方が理解できる標準構文であるため、開発前にこの仕様を通じて実装内容を確認し、開発完了後にLLMがこの仕様を読み取り、MCPを使ってブラウザやモバイルを操作して受け入れテストを実行できます。詳細なデモンストレーションは、以下のYouTube動画をご覧ください。
これにより、LLMとのコミュニケーションツールとして使えるだけでなく、受け入れ条件を満たしていないことを発見した場合、LLMが観察して実装を修正することもできます。
興味があれば、GitHubで試してみてください。yurenju/llm-bdd-coding-demo
BDD + TDD
BDDは、より明確な仕様と受け入れ条件により、期待通りの成果が得られない問題を軽減できますが、開発者の認知過負荷の問題は解決できません。段階的なTDDを組み合わせることで、この問題を緩和できます。
BDDを使用すると、開発仕様と受け入れ基準を明確に定義できますが、LLM開発で頻繁に遭遇するもう一つの問題があります。それは、LLMがあまりにも速くコードを生成することです。一度に生成される内容が認知負荷を超えると、誘惑に負けて確定ボタンをそのまま押してしまいますが、注意深く見ないと意図しない内容が生成されることがあります。
この認知負荷を解決するため、最近はBDD + TDDをテストしています。BDD部分は前述のようにGherkinを受け入れ基準として使用します。しかし、LLMにコンポーネントを分解してもらい、各コンポーネントの開発時に以下の順序を守るよう依頼します。
- まずインターフェース、空のクラス、または空の関数を記述し、throw new Error('not implemented yet')のような未実装エラーをスローする
- テストの記述のみを書いてもらう。つまり、自動テストのdescribe('記述')とit('記述')だけで、テストロジックは実装せず、確認させる
- この段階で、どの程度のテストを書くつもりか把握でき、テストの粒度について直接コミュニケーションを取ります。通常、テスト項目を大幅に削減します。一般的に細かすぎるからです
- テスト項目を確認した後、テストロジックを記述してもらう
- テストを実行する。この時点で新しく追加されたテストはすべて失敗すべきです(レッドフェーズ)
- 実装を開始してもらい、実装後にテストを実行します。理論的には、書いたテストがすべてパスするはずです(グリーンフェーズ)
このような開発フローでは、各段階の成果物が認知負荷の範囲内に収まり、成果物を適切に確認できます。そして、「何が正しいか」が明確になった後は、BDDフローと同様に、明確な完了条件があればLLMは優れた成果を出せます。
このような開発フローに興味がある方は、以前書いたyurenju/cursor-tdd-rulesを参照してください。Claude Codeで使用する場合は、若干の修正が必要です。
ただし、これらはまだ発展途上の協働開発方法であることを忘れないでください。ツールや使用技術は急速に更新されており、すぐに適用できなくなる可能性もあります。
このような開発方法を使用する主な目的は、自分の認知負担を軽減し、プロジェクトを自分のコントロール下に置きながら、できる限りLLMを活用して目的を達成することです。同時に、境界と目標を明確にすることで、LLMとより良くコミュニケーションを取り、自分の目標が何であるかを伝えられます。
この過程で、開発初期から自分が何を望んでいるかがより明確になると感じています。LLMとの協働も人間との協働も、秘訣はほぼ同じです。より頻繁なコミュニケーションと要件の確認です。
おそらく人間との協働とそれほど大きな違いはなく、自分のコミュニケーション能力を強化することが重要なのでしょう。