テスト駆動開発(TDD)ではテストリスト、あるいはTODOリストを作ります。テストリストは実現したいことの全体を網羅しつつ、問題を着手できるくらい小さな項目に分割しながら、一度に1テストずつ進めていくためのツールです。開発を一歩ずつ、1テストずつ進めながら、最後にはゴールにたどり着けるような道のりを示すものです。漫然とテストを書くだけではゴールを見失いますし、実装に夢中になっていると道を踏み外します。進む方向と進み方をセルフコントロールするためにもテストリストは重要です。
ところがテストリストを作るのは難しいものです。上手にテストリストを作るには、仕様を漏れなく重複なく分割し、さらに詳細化し、設計をイメージできるところはそれに従って項目を挙げ、イメージできない箇所は手探りで進む計画を立て、仕様に対し妥当な具体例を構築し、同時にエッジケースや例外ケースも適度に含めていき、そして想定から外れたらリストに戻って考え直すことになります。ひとつひとつ取っても経験をスキルを要しますし、いずれも見失わず総合的に考えないといけないし、さらに最初から完璧にしようとせず「進みながら考える」必要もあります。TDDを続ければ上達しますしササッと書けるようになりますが、とりわけ最初のうちは延々と悩んでしまい、時間がかかってしまうこともあります。
人間が時間をかければできる作業、これは生成AIの出番ではないでしょうか。おなじみFizzBuzz問題を使って、TDDのためのテストリストをAIに作ってもらいましょう。
プロンプト 以下のプログラミングのお題を読んでください。 「1から100までの数をプリントするプログラムを書け。ただし、3の倍数のときは数の代わりに「Fizz」と、5の倍数のときは「Buzz」とプリントし、3と5両方の倍数の場合には「FizzBuzz」とプリントすること。」 この仕様をもとに、テスト駆動開発(TDD) で進めるためのテストリストを作ってください。TDDのテストリストはインデントを用いた箇条書きで、トップレベルは全体的で、ボトムレベルは詳細で具体的になるようにします。
結果はこうでした。
ChatGPT o1-preview: - **数をプリントする基本機能** - 数字をそのままプリントするテスト - 入力: 1、期待出力: "1" - 入力: 2、期待出力: "2" - **3の倍数のときに「Fizz」をプリントする機能** - 3の倍数で「Fizz」をプリントするテスト - 入力: 3、期待出力: "Fizz" - 入力: 6、期待出力: "Fizz" - **5の倍数のときに「Buzz」をプリントする機能** - 5の倍数で「Buzz」をプリントするテスト - 入力: 5、期待出力: "Buzz" - 入力: 10、期待出力: "Buzz" - **3と5の両方の倍数のときに「FizzBuzz」をプリントする機能** - 15の倍数で「FizzBuzz」をプリントするテスト - 入力: 15、期待出力: "FizzBuzz" - 入力: 30、期待出力: "FizzBuzz" - **1から100までの数を正しくプリントする機能** - ループが正しく100回実行されるテスト - 全ての条件を組み合わせた総合テスト - 入力: 1から100、期待出力: 仕様に従った結果
良さそうですね。ただし生成AIのひとたちは、FizzBuzzのような有名な問題を「知っている」ので、生成するというよりは学習データからそのまま持ってくる傾向があるようです。私が自作した、もう少し複雑な問題も試してみます。
プロンプト 以下のプログラミングのお題を読んでください。 「プレイヤー同士が1対1で対戦するシンプルなトレーディングカードゲーム(TCG)を実装してください。各プレイヤーは20のライフポイントと、固定の5枚のカードデッキを持ちます。各カードには攻撃力(1~5)が設定されています。ターン制で進行し、プレイヤーはターンごとに1枚のカードを使用して相手にダメージを与えます。ライフが0以下になったプレイヤーが敗北します。デッキのシャッフルや、ターンの管理も行う必要があります。」 この仕様をもとに、テスト駆動開発(TDD) で進めるためのテストリストを作ってください。TDDのテストリストはインデントを用いた箇条書きで、トップレベルは全体的で、ボトムレベルは詳細で具体的になるようにします。
ChatGPTとClaudeを試してみました (結果は下にあります)。いずれも全体を網羅しつつ機能要素ごとに分割してあり、なかなか良さそうです。
気になるのは着手順です。どちらも初期化を最初にしています。最終的に完成したプログラムは初期化から始まるでしょうが、TDDの最初の一手が初期化というのはあまり良くありません。初期化する対象が分かっていれば初期化できますが、まだ対象は書けていないし、分からないわけです。プレイヤーやカードがあると言われるとそういうPlayerクラスやCardクラスを作りたくなるかもしれませんが、それが問題に対し最もシンプルとは限りません。
TDDで実装と設計を進める流れとして、和田卓人さんはテスト容易性と重要度で並べ替えるよう指導しています。重要度が低いものは後回しにして良い。テスト容易なものは着手しやすいから先にやりましょう。重要だがテスト容易性が低いものは、テストしたい箇所を引きはがしてテストしやすい設計にします(Humble Objectパターン)。このように考えてリストを更新すると、手が止まらずに済みますし、着実に前進しやすくなります。
いっぽうケント・ベックは "Known to unknown" と言っています。わかるところ、自明なところから着手し、テストとリファクタリングを通じてきれいな動くコードを書きます。これは安全地帯のようなもので、仕様も設計も十分理解できていて、コードも大丈夫です。ここを土台として次の一手は、少し分からないところに進みます。分からなくなったら安全地帯に戻って、落ち着いて別の手を考えます。
どこがわかる部分で、どこがわからないかは人によって違います。唯一の正解はなく、1人ずつにとって上手くいく流れがあります。私だったらまず仕様の核心から着手し、できるだけシンプルなコードで実現して、そこから少しずつ仕様追加しながら設計を改善していきたいなあと思います。自分の仕様理解とコード設計を、ぴったり合わせながら進めたい。 このような考え方をもって、自分でテストリストを並び替えましょう。自分で考えながら、重要なもの、テストが容易なもの、わかるものとわからないものを見分けていくこと自体が、頭の中で設計を組み立てることにつながります。
とはいえテストリストを作るのは難しいものなのでした。並び順も生成AIに少しは考えてもらいましょう。そのようにプロンプトに追加します。
プロンプト 以下のプログラミングのお題を読んでください。 「プレイヤー同士が1対1で対戦するシンプルなトレーディングカードゲーム(TCG)を実装してください。各プレイヤーは20のライフポイントと、固定の5枚のカードデッキを持ちます。各カードには攻撃力(1~5)が設定されています。ターン制で進行し、プレイヤーはターンごとに1枚のカードを使用して相手にダメージを与えます。ライフが0以下になったプレイヤーが敗北します。デッキのシャッフルや、ターンの管理も行う必要があります。」 この仕様をもとに、テスト駆動開発(TDD) で進めるためのテストリストを作ってください。TDDのテストリストはインデントを用いた箇条書きで、トップレベルは全体的で、ボトムレベルは詳細で具体的になるようにします。また以下のような性質を持つように作ります。 -テストケースではなく、実装を進めるために仕様・機能を取り扱いやす大きさで切り出して並べる -全体を網羅できるようトップレベルの項目を出してから、進める順序に応じて細分化していく -トップレベルは、機能分割の場合と、状況や場合で分割する場合があり、適切なアプローチを選ぶ -どうなるかわからない箇所、イメージしにくい箇所、リスクがある箇所をできるだけ早く着手する。As Kent Beck put it, "Known to Unknown, One step at a time." - 実装するものの全体像をできるだけ早くプログラマーが把握できるように、仕様から最低限の部分を抽出して全体構成を早いタイミングで導く。よくないアプローチの例として、部品を先にすべて作ってから組み合わせる、1機能の仕様を完全に作り上げてから次の機能に進む、網羅的なテストケースを先に作ろうとする、などがある -着手するものは具体的なテストケースにする -各項目は説明的に、内容と意図がわかるように記述し、あまり簡潔にしない
ChatGPTとClaudeの新しい回答はこちらです。どちらもやはり、プレイヤーやカードの情報を持つところを最初に持ってくるので、初期化から着手っぽい傾向が強いようです。とはいえ情報が取得できないとテストも書けないので、このへんが最初に来るのはそれほど悪くないかもしれません。
あとは自分がやりやすいテストになるよう、生成AIとチャットを続けて修正してもらいましょう。人間とペアプログラミングするのとよく似ている感触があります。よくわからない項目は説明してもらえばいいし、順序を変えるときに理由まで伝えるとうまく整理してくれます。テストリストはTDDを進めながらどんどん更新し、変化していくものなので、そこでもときどき生成AIと相談するとよさそうです。
今回は、テスト駆動開発のテストリストを最初に書き出す部分を、生成AIに支援してもらいました。そのまま使えるかは微妙ですが、できたリストを理解したり、さらに生成AIに説明してもらったり修正したりしていくと、十分に使えるものになりそうです。
私 (やっとむ) はクリエーションライン株式会社と共同してTDDと生成AIの利活用について調査研究しており、今回のエントリはその一環です。クリエーションライン Tech Blogに同内容を掲載予定です。TDDと生成AIの活用について、一緒に試していただける方を募集しています。関心ある方はコメントをお願いします。
最初のプロンプトの結果
ChatGPT o1-preview:
プレイヤーの初期化
- プレイヤーはライフポイント20で初期化されることをテストする
- プレイヤーのデッキが5枚のカードで構成されていることをテストする
- 各カードの攻撃力が1から5の範囲内であることをテストする
デッキのシャッフル
- ゲーム開始時にデッキがシャッフルされることをテストする
- シャッフル前後でデッキの順序が変化していることを確認する
- デッキ内のカードが全て存在していることを確認する
ターンの管理
- ゲームがターン制で進行することをテストする
- プレイヤーが交互にターンを行うことをテストする
- 各ターンでプレイヤーが1枚のカードを使用することをテストする
- 使用したカードがデッキから取り除かれることをテストする
ダメージの計算
- カード使用時に相手のライフがカードの攻撃力分だけ減少することをテストする
- 例:攻撃力3のカードで相手のライフが3減ることを確認する
- ライフポイントが0未満になった場合でも正しく計算されることをテストする
勝敗の判定
- プレイヤーのライフが0以下になったときにゲームが終了することをテストする
- ライフが0以下になったプレイヤーが敗北することをテストする
- 勝者が正しく判定されることをテストする
ゲームの流れ
- ゲーム開始時にデッキをシャッフルすることをテストする
- 先攻・後攻が正しく決定されることをテストする
- プレイヤーが勝敗が決まるまで交互にターンを行うことをテストする
エッジケースのテスト
- 両プレイヤーのライフが同時に0以下になった場合の処理をテストする(引き分けの判定)
- デッキが空になった場合の処理をテストする(5ターン後にデッキがなくなるシナリオ)
ユーザーインターフェースのテスト(もし適用可能なら)
- プレイヤーのライフポイントが正しく表示されることをテストする
- 使用可能なカードが正しく表示されることをテストする
- ゲーム終了時に勝者が正しく表示されることをテストする
エラーハンドリングのテスト
- 無効なカードを使用しようとした場合の処理をテストする
- ターン外で行動しようとした場合の処理をテストする
Claude 3.5Sonnet:
トレーディングカードゲーム テストリスト
ゲーム初期化のテスト
プレイヤーの初期化
- プレイヤーが20ライフポイントで開始されること
- プレイヤーが5枚のカードデッキを持つこと
- 各プレイヤーのデッキが異なる順序でシャッフルされること
カードの初期化
- カードが1から5の攻撃力を持つこと
- カードが適切な識別子を持つこと
- 不正な攻撃力のカードが作成できないこと
ゲームの進行に関するテスト
ターン管理
- 最初のプレイヤーが正しく選択されること
- プレイヤーが交互に行動できること
- 各ターンで1枚だけカードが使用できること
- 使用済みのカードが再利用できないこと
カードの使用
- カードを使用すると相手のライフポイントが減少すること
- カードの攻撃力分だけダメージが与えられること
- カードを使用するとデッキから除外されること
- 存在しないカードは使用できないこと
ゲーム終了条件のテスト
勝利条件
- プレイヤーのライフが0以下になった時点で対戦が終了すること
- 正しい勝者が判定されること
- 同時に両者のライフが0以下になった場合の処理
エッジケース
- デッキが空になった場合の処理
- 不正な操作(順番外でのカード使用など)が防止されること
- ゲーム途中での状態確認が可能なこと
統合テスト
- 一連のゲームプレイ
- 開始から終了までの一連の流れが正常に動作すること
- 複数回のゲームを連続して実行できること
- ゲーム履歴が正しく記録されること
2回目のプロンプトの結果
ChatGPT o1-preview:
プレイヤーの初期化
- プレイヤーは20のライフポイントを持つ
- プレイヤーのライフポイントが正しく20で初期化されることを確認する
- プレイヤーは5枚の固定デッキを持つ
- デッキが5枚のカードで構成されていることを確認する
- 各カードが攻撃力を持つことを確認する
カードの特性
- カードは攻撃力を持つ
- 攻撃力が1以上5以下の範囲内であることを確認する
- 攻撃力が整数であることを確認する
デッキのシャッフル
- デッキをシャッフルできる
- シャッフル後にデッキの順序が変化することを確認する
- シャッフルしてもデッキ内のカード枚数が変わらないことを確認する
ターンの管理
- ゲームはターン制で進行する
- プレイヤーが交互にターンを行うことを確認する
- ターンの開始時に正しいプレイヤーが行動できることを確認する
カードの使用
- プレイヤーは自分のターンに1枚のカードを使用できる
- 手札からカードを選択して使用できることを確認する
- 使用したカードがデッキから取り除かれることを確認する
- 同じターンに複数のカードを使用できないことを確認する
ダメージの適用
- カードの攻撃力分だけ相手にダメージを与える
- 相手プレイヤーのライフポイントが正しく減少することを確認する
- ダメージ計算が正確に行われることを確認する
ライフポイントの管理
- プレイヤーのライフポイントが0以下になった場合の処理
- ライフポイントが0になった場合に敗北が判定されることを確認する
- ライフポイントが負の値にならないように制御することを確認する
勝敗の判定
- ライフポイントが0以下のプレイヤーが敗北する
- 勝者が正しく判定されることを確認する
- ゲーム終了時に結果が表示されることを確認する
ゲームの進行
- 全体のゲームフローが正しく動作する
- ゲーム開始から終了まで一連の流れが途切れないことを確認する
- デッキがなくなった場合の処理を確認する
デッキの管理
- デッキからカードを引く際の処理
- デッキが空の場合にカードを引けないことを確認する
- デッキから正しくカードが引かれることを確認する
エッジケースの検証
- 不正な値や状態への対処
- 攻撃力が範囲外のカードが存在しないことを確認する
- プレイヤーのライフポイントが不正な値にならないことを確認する
- プレイヤーへの情報提示
- 現在のライフポイントとデッキ枚数が表示されることを確認する
- ターンの進行状況がわかるように表示されることを確認する
- 使用可能なカードの情報が表示されることを確認する
入力と操作
- プレイヤーがカードを選択して使用できる
- カード選択の入力が正しく受け付けられることを確認する
- 不正な入力に対して適切なエラーメッセージが表示されることを確認する
ランダム性の検証
- デッキのシャッフルによるランダム性
- シャッフル結果が毎回異なることを確認する
- シャッフルが偏っていないことを統計的に確認する
複数ゲームの連続実行
- ゲームを再度開始できる
- ゲーム終了後に新しいゲームを開始できることを確認する
- 前のゲームの状態が新しいゲームに影響しないことを確認する
パフォーマンスの検証
- ゲームの処理が遅延なく行われる
- 各ターンの処理時間が許容範囲内であることを確認する
- 大量のゲームを連続して行った場合でも性能が劣化しないことを確認する
Claude 3.5Sonnet:
ゲームの基本構造と重要な要素を踏まえつつ、TDDで効率的に進められるようなテストリストを作成します。
プレイヤー対戦TCGのテストリスト
1. ゲームの基本構成要素を最小限で作成
- 2人のプレイヤーが存在し、それぞれが名前とライフポイント(20)を持つことを確認
- プレイヤーを作成できる
- プレイヤーの初期ライフが20である
- プレイヤー名を取得できる
- カードの基本構造を実装
- カードは攻撃力(1-5)を持つ
- カードの攻撃力を取得できる
2. デッキの基本機能
- 5枚のカードでデッキを作成できる
- デッキのカード枚数が5枚である
- デッキから特定のカードを参照できる
- デッキのシャッフル機能
- シャッフル前後でカードの総数が変わらない
- シャッフル後にカードの順序が変更されている
- 複数回シャッフルしても正しく動作する
3. 最小限のゲームの進行機能
- プレイヤーが交互にターンを実行できる
- 現在のターンプレイヤーを取得できる
- ターンを次のプレイヤーに渡せる
- 基本的なカードプレイの流れ
- プレイヤーがカードを使用できる
- カードの使用により相手のライフが減少する
- 使用したカードがデッキから除外される
4. 勝利条件の判定
- ライフポイントによる勝敗判定
- プレイヤーのライフが0以下になった時点で負けとなる
- 勝者が正しく判定される
- 両者のライフが1以上の場合はゲームが継続する
5. エラー処理と例外ケース
- 不正な操作の防止
- ターン外のプレイヤーはカードを使用できない
- 既に使用済みのカードは使用できない
- 存在しないカードインデックスを指定した場合にエラーとなる
6. ゲーム全体の流れ制御
- ゲームの開始から終了までの一連の流れ
- ゲームを正しく初期化できる(プレイヤー作成、デッキ配布、シャッフル)
- ターンを正しく進行できる
- 勝敗が決まった時点でゲームが終了する
7. 状態の表示と取得
- 現在のゲーム状態の確認
- 各プレイヤーの現在のライフポイントを表示できる
- 残りのデッキ枚数を確認できる
- 現在のターンプレイヤーを確認できる
このリストの特徴: 1. 最小限の機能から開始し、徐々に機能を追加する構成 2. デッキのシャッフルという不確実な要素を早めに実装 3. 基本的なゲームの流れを早い段階で確認できる 4. エラーケースは基本機能が動作した後で実装 5. 各項目は具体的な振る舞いを示し、実装の方向性が明確 6. テストの意図が理解しやすいよう、説明的な記述を心がけた
最初に着手すべきテストケース:
python def test_create_player(): player = Player("Player1") assert player.name == "Player1" assert player.life == 20