Amazon Web Services ブログ

Kiro でテスト駆動開発(TDD):こうあるべき体験

本記事は 2026 年 5 月 21 日に公開された Mike George 氏の “Test Driven Development (TDD) with Kiro: this is how it should feel” を翻訳したものです。

私のキャリアの初期に、所属していた組織は、コード品質の向上と自信を持ってリファクタリングできる体制づくりのために、本格的にユニットテストを導入するという正しい判断を下しました。私たちは テスト駆動開発(TDD) の導入を試みました。その利点は理解していたものの、TDD を実践する作業自体が負担に感じられ、エンジニアたちにこの手法を一貫して適用してもらえませんでした。私自身、TDD というアイデアは大好きでしたが、実際の作業は嫌いでした。本記事では、Kiro を使って TDD を実践する方法を紹介し、red-green-refactor サイクルに沿ったテストを手作業で書くという苦痛を伴うことなく、TDD の恩恵を受ける方法をお見せします。

テスト駆動開発(TDD)の概要

テスト駆動開発の基本的な考え方は次のとおりです。

  1. 構築する新機能ごとに、その新機能の振る舞いを特定します。
  2. 新しい振る舞いそれぞれに対して、もしその振る舞いが存在すればパスするはずのテストを書きます。
  3. テストを実行します。振る舞いがまだ存在しないため、すべてのテストは失敗するはずです。
  4. 新しいテストをパスさせるもっともシンプルなコードを書きます。
  5. すべてのテストがパスし続けることを確認しながら、必要に応じてリファクタリングします。

これが red-green-refactor サイクルと呼ばれるものです。書いたテストが失敗し(red フェーズ)、テストをパスさせるためにコードを書き(green フェーズ)、必要に応じてリファクタリングを続ける(refactor サイクル)という流れです。TDD は高品質なコードを生み出すのに役立ちますが、時間がかかると見られがちで、プロセスを一貫して守るためにかなりの規律が求められます。

私は TDD のアイデアは大好きでしたが、テストコードと実装コードの間を行き来するためのコンテキストスイッチが嫌いでした。どのテストを書いたかを管理することも好きではなく、率直に言って、TDD をきちんと実践するために必要なすべてのテストを書く単調さも嫌でした。TDD の経験について他の人と話すと、自分だけがそう感じているわけではないことがよくわかります。

Kiro のようなエージェント型開発ツールは、TDD のこうした欠点をうまく扱ってくれます。Kiro は仕様駆動開発をサポートしています。これは、Kiro が正しいものを正しい方法で正しいアーキテクチャに沿って構築できるよう導く、3 つのフェーズからなる開発プロセスです。各 spec は次の 3 つのフェーズで構成されます。要件フェーズでは、Kiro がユーザーストーリーと受け入れ基準の定義と承認を支援します。設計フェーズでは、技術アーキテクチャと実装アプローチを定義します。タスクフェーズでは、元の要件を満たすコードを生み出すための実行可能な実装タスクを定義します。

Kiro は仕様駆動開発に加えて、hooks もサポートしています。hooks は、IDE で特定のイベントが発生したときに自動的に実行される自動化ツールです。たとえば、ファイルが保存されるたびにコードを lint する hook を作成したい場合などです。要するに、仕様駆動開発が全体的なエンジニアリングのアプローチを構造化するのに役立つ一方、hooks は TDD のような特定のプラクティスの徹底を可能にします。

私の場合は、TDD の取り組みを支援するために Kiro hook を作成しました。

本番環境での利用におけるセキュリティ上の考慮事項

このチュートリアルは、教育目的で例示プロンプトを使用して Kiro の TDD 機能を紹介しています。本番システム向けにこのアプローチを適用する場合は、次の点に留意してください。

  • デプロイ前に、AI が生成したコードのセキュリティ脆弱性を必ずレビューしてください。とくに、認証、認可、入力検証、データ処理ロジックには注意が必要です。コードが正しく動作することを確認するため、さまざまなシナリオでテストしてください。
  • この例では簡潔さのために認証なしのオープンな API を使っていますが、本番環境のデプロイには包括的なセキュリティ対策が必要です。具体的には、認証と認可、HTTPS/TLS 暗号化、保存時の暗号化、入力検証、レート制限、包括的なロギング、認証情報のシークレット管理などです。
  • AI 開発ツールを採用する組織は、アクセス制御、コード品質モニタリング、検証プロセスに関するガバナンスポリシーを確立する必要もあります。

包括的なセキュリティガイダンスについては、所属組織のセキュリティチームに相談し、AWS のセキュリティのベストプラクティスに関するドキュメント を参照してください。

Kiro と TDD の統合

Kiro 内で hook を作成するには、Kiro タブに移動し、agent hooks ウィンドウの「+」ボタンをクリックして manually create a hook を選択します。表示されたウィンドウで、以下を入力します。

  • TitleTDD: Test First
  • Description本番コードを書く前に、失敗するテストを書くよう促すことでテスト駆動開発を徹底します。red/green/refactor サイクルに従います。すなわち、失敗するテストを書き(red)、それをパスさせる最小限のコードを書き(green)、その後リファクタリングします。
  • EventPre Tool Use
  • Tool namewrite
  • ActionAsk Kiro
  • Instructions for Kiro agent(以下は和訳です。原文は英語で記述されています):
待ってください! あなたは今コードを書こうとしています。TDD に従い、まずテストを書いてください。

必要なアクション:
1. これがテストファイル(*test*.py、*.spec.* など)の場合 — 続行する
2. これが例外(設定ファイル、ドキュメント、.kiro/)の場合 — 続行する
3. 対応するテストファイルが存在し、かつ実行した結果すべてのテストが失敗(RED フェーズ)していることが確認できている場合 — 続行する
4. それ以外の場合 — 中断し、まずテストファイルを書く:
   a. test_<filename>.py(または同等のもの)を作成する
   b. (単なる import ではなく)実際のアサーションで失敗するテストを書く
   c. テストを実行し、すべてがアサーションエラーで失敗することを確認する
   d. その後、戻ってこの実装を書く

TDD サイクル:
- RED: まず失敗するテストを書く(新しいテストはすべてアサーションで失敗しなければならない)
- GREEN: テストをパスさせる最小限のコードを書く
- REFACTOR: テストが green のままになるようにしつつコードを整理する

RED フェーズの要件:
- すべてのテストは AssertionError またはテスト失敗で失敗しなければならない
- RED フェーズでいずれかのテストがパスする場合、そのテストは弱い/中身がない
- RED フェーズでテストがパスするのは、空のループ、アサーションの欠落、テスト内のロジックエラーを意味する

正しい RED フェーズ: すべてのテストが AssertionError で失敗している
不正な RED フェーズ: 一部のテストがパスする、ImportError、ModuleNotFoundError、構文エラー

例外(テストなしで続行してよいもの):
- テストファイル: *test*.py、*.spec.*、*.test.*、test_*.py、*_test.go
- 設定: package.json、tsconfig.json、.eslintrc、webpack.config.js、vite.config.ts、jest.config.js、pytest.ini、setup.py、pyproject.toml、requirements.txt、Dockerfile、docker-compose.yml、.env、.gitignore
- .kiro/ ディレクトリ内のファイル
- 型定義: .d.ts ファイル、Protocol クラス
- ドキュメント: .md、.txt、.rst、LICENSE、README、CHANGELOG
- データファイル: .json、.yaml、.yml、.xml、.csv、.sql、.html、.css、.scss(ロジックを含まないもの)
- ビルド成果物: dist/、build/、__pycache__/、*.pyc、node_modules/

先にテストを必要とするもの:
- 関数/クラスを含む Python ファイル(テストパターンに一致しない .py)
- ロジックを含む JavaScript/TypeScript(テストパターンに一致しない .js、.ts、.jsx、.tsx)
- 実行可能なロジックを含むすべてのソースコード(Go、Java、C#、Ruby など)

重要な確認: テストを実行し、すべてのテストが失敗したことを確認しましたか? スタブ実装でいずれかのテストがパスした場合は、まずテストを修正してください。すべてのテストが失敗する適切な RED フェーズが得られたときにのみ続行してください。

画面の一番下までスクロールし、Create Hook をクリックします。

この hook は、Kiro がファイルを保存しようとするたびに実行されます。コードを書いている過程で red-green-refactor サイクルが守られているかをチェックします。

簡単なテストとして、モンティ・ホール問題 を示すプログラムを書くよう Kiro に依頼してみました。Kiro はユニットテストを書き、それらは失敗します。次に Kiro は、対応するモジュールがまだ存在しないためにテストが失敗していることに気づきます。hook によれば、テストはアサーションの失敗によって失敗しなければなりません。そこで Kiro は基本的なモジュールを作成し、テストがアサーションエラーで失敗することを確認します。その後、テストをパスさせるための最小限のコードを書きます。

このシンプルな例で hook が機能していることがわかりますが、もう少し現実的なもの、つまり本番環境向けの REST ベースの API の構築でどう動くか見てみましょう。

REST ベースの API を構築する

実際のコードを書くため、私は Kiro の spec セッション を開始し、タスク管理システム用の REST ベースの API を構築するための要件を入力しました。最終的な要件ドキュメント(以下は和訳です。原文は英語で記述されています)は以下のとおりです。

# 要件ドキュメント

## はじめに

タスク管理システム用の REST API です。この API は、永続化のための
リレーショナルデータベースまたは組み込みデータベース(PostgreSQL や
SQLite など)を備えた Python の Web フレームワーク(Flask や
FastAPI など)を使って構築します。アプリケーションは Docker コンテナ
としてパッケージ化されるため、コンテナをサポートする任意の
プラットフォームにデプロイできます。この API はオープン(認証・認可
なし)です。タスクに対する完全な CRUD 操作をサポートし、クライアントが
タスクを作成・読み取り・更新・削除できるようにします。フロントエンドは
ありません。これは API のみのサービスです。

## 用語集

- **API_Server**: HTTP リクエストを受け取り、適切なリクエスト
  ハンドラーへルーティングする Python の Web フレームワーク
  アプリケーション(Flask や FastAPI など)
- **Task_Service**: タスク関連のビジネスロジックの処理を担う、
  コンテナ内で動作するアプリケーションロジック
- **Task**: 一意の識別子、タイトル、説明、ステータス、
  タイムスタンプといったプロパティを持つ作業単位
- **Task_Store**: タスクが格納・取得されるリレーショナル
  データベースまたは組み込みデータベース(PostgreSQL や SQLite など)
- **Client**: API に HTTP リクエストを送信する外部システム
  またはユーザー
- **Dockerfile**: アプリケーションとその依存関係をポータブルな
  Docker イメージにパッケージ化する、コンテナのビルド定義
- **Container**: API_Server と Task_Service をホストする、
  Docker イメージの実行中インスタンス
- **Standard_Error_Format**: すべての API エラーレスポンスで
  使われる一貫した JSON エラーレスポンス構造。
  `{"error": "<説明的なメッセージ>"}` の形式

## 要件

### 要件 1: タスクの作成

**ユーザーストーリー:** クライアントとして、API を通じて
新しいタスクを作成したい。作業項目を追跡できるようにするためである。

#### 受け入れ基準

1. WHEN クライアントが title と description の両方を含む有効な
   リクエストボディとともに `/tasks` へ POST リクエストを送信する、
   THE Task_Service SHALL Task_Store に新しい Task を作成し、
   一意の識別子を持つ作成済みの Task を HTTP ステータス 201 とともに返す
2. WHEN クライアントが必須フィールド(title または description)が
   欠落したリクエストボディとともに `/tasks` へ POST リクエストを
   送信する、THE Task_Service SHALL Standard_Error_Format の
   エラーメッセージとともに HTTP 400 レスポンスを返す
3. THE Task_Service SHALL 新しく作成された各 Task に、一意の識別子、
   `createdAt` タイムスタンプ、`updatedAt` タイムスタンプを割り当てる
4. THE Task_Service SHALL 新しい Task の初期ステータスを
   "pending" に設定する

### 要件 2: 単一タスクの取得

**ユーザーストーリー:** クライアントとして、識別子を指定して
特定のタスクを取得したい。その詳細を閲覧できるようにするためである。

#### 受け入れ基準

1. WHEN クライアントが有効なタスク識別子とともに
   `/tasks/{taskId}` へ GET リクエストを送信する、
   THE Task_Service SHALL 一致する Task を HTTP ステータス 200
   とともに返す
2. WHEN クライアントが Task_Store に存在しないタスク識別子とともに
   `/tasks/{taskId}` へ GET リクエストを送信する、
   THE Task_Service SHALL Standard_Error_Format の
   エラーメッセージとともに HTTP 404 レスポンスを返す

### 要件 3: 全タスクの一覧取得

**ユーザーストーリー:** クライアントとして、すべてのタスクを
一覧表示したい。追跡中の全作業項目の概要を確認できるように
するためである。

#### 受け入れ基準

1. WHEN クライアントが `/tasks` へ GET リクエストを送信する、
   THE Task_Service SHALL Task_Store のすべての Task のリストを
   HTTP ステータス 200 とともに返す
2. WHEN Task_Store に Task が 1 つも存在しない、
   THE Task_Service SHALL 空のリストを HTTP ステータス 200 とともに返す

### 要件 4: タスクの更新

**ユーザーストーリー:** クライアントとして、全体を置き換える形で
既存のタスクを更新したい。タイトル、説明、ステータスを変更できるように
するためである。

#### 受け入れ基準

1. WHEN クライアントが有効なタスク識別子と、すべての必須フィールド
   (title、description、status)を含むリクエストボディとともに
   `/tasks/{taskId}` へ PUT リクエストを送信する、
   THE Task_Service SHALL Task_Store 内の一致する Task を完全に
   置き換え、更新後の Task を HTTP ステータス 200 とともに返す
2. WHEN クライアントが Task_Store に存在しないタスク識別子とともに
   `/tasks/{taskId}` へ PUT リクエストを送信する、
   THE Task_Service SHALL Standard_Error_Format の
   エラーメッセージとともに HTTP 404 レスポンスを返す
3. WHEN クライアントが必須フィールド(title、description、status)
   のいずれかが欠落したリクエストボディとともに `/tasks/{taskId}` へ
   PUT リクエストを送信する、THE Task_Service SHALL
   Standard_Error_Format のエラーメッセージとともに HTTP 400
   レスポンスを返す
4. THE Task_Service SHALL Task が変更されるたびに `updatedAt`
   タイムスタンプを更新する

### 要件 5: タスクの削除

**ユーザーストーリー:** クライアントとして、タスクを削除したい。
不要になった作業項目を完全に除去できるようにするためである。

#### 受け入れ基準

1. WHEN クライアントが有効なタスク識別子とともに
   `/tasks/{taskId}` へ DELETE リクエストを送信する、
   THE Task_Service SHALL Task_Store から Task を完全に削除し、
   HTTP ステータス 204 を返す
2. WHEN クライアントが Task_Store に存在しないタスク識別子とともに
   `/tasks/{taskId}` へ DELETE リクエストを送信する、
   THE Task_Service SHALL Standard_Error_Format の
   エラーメッセージとともに HTTP 404 レスポンスを返す

### 要件 6: リクエストの検証

**ユーザーストーリー:** クライアントとして、不正なリクエストに
対して明確なエラーレスポンスがほしい。API 呼び出しの問題を
修正できるようにするためである。

#### 受け入れ基準

1. WHEN クライアントが無効な JSON ボディを含むリクエストを送信する、
   THE API_Server SHALL Standard_Error_Format の
   エラーメッセージとともに HTTP 400 レスポンスを返す
2. WHEN クライアントが未定義のルートへリクエストを送信する、
   THE API_Server SHALL HTTP 404 レスポンスを返す
3. WHEN クライアントが定義済みのルートに対してサポートされていない
   HTTP メソッドを使ってリクエストを送信する、THE API_Server SHALL
   HTTP 405 レスポンスを返す

### 要件 7: 入力長の検証

**ユーザーストーリー:** クライアントとして、API に妥当な入力長を
強制してほしい。過度に大きな入力が拒否されるようにするためである。

#### 受け入れ基準

1. WHEN クライアントが 100 文字を超える `title` フィールドを含む
   リクエストを送信する、THE Task_Service SHALL title が長すぎる
   ことを示す Standard_Error_Format のエラーメッセージとともに
   HTTP 400 レスポンスを返す
2. WHEN クライアントが 1000 文字を超える `description` フィールドを
   含むリクエストを送信する、THE Task_Service SHALL description が
   長すぎることを示す Standard_Error_Format のエラーメッセージと
   ともに HTTP 400 レスポンスを返す
3. THE Task_Service SHALL 作成(POST)操作と更新(PUT)操作の
   両方で入力フィールドの長さを検証する

### 要件 8: タスクのデータモデル

**ユーザーストーリー:** クライアントとして、一貫したタスクの
データ構造がほしい。API レスポンスを確実にパースできるように
するためである。

#### 受け入れ基準

1. THE Task_Service SHALL 各 Task を次のフィールドを持つ JSON
   オブジェクトとして表現する: `id`(string)、`title`(string)、
   `description`(string)、`status`(string)、
   `createdAt`(ISO 8601 タイムスタンプ)、
   `updatedAt`(ISO 8601 タイムスタンプ)
2. THE Task_Service SHALL Task の status フィールドとして次の値を
   受け入れる: "pending"、"in-progress"、"completed"
3. IF クライアントが受け入れ可能な集合に含まれない status 値を
   指定する、THEN THE Task_Service SHALL Standard_Error_Format の
   エラーメッセージとともに HTTP 400 レスポンスを返す

### 要件 9: JSON シリアライズのラウンドトリップ

**ユーザーストーリー:** クライアントとして、シリアライズと
デシリアライズを通じて API がタスクデータを忠実に保持してほしい。
データが失われたり破損したりしないようにするためである。

#### 受け入れ基準

1. FOR ALL 有効な Task オブジェクトについて、Task を JSON へ
   シリアライズし、その JSON を再び Task へデシリアライズすると、
   同等の Task オブジェクトが得られる SHALL(ラウンドトリップ特性)
2. THE Task_Service SHALL すべてのリクエストボディとレスポンス
   ボディの唯一のデータ交換形式として JSON を使用する
3. THE Task_Service SHALL すべてのレスポンスに
   `Content-Type: application/json` ヘッダーを含める

### 要件 10: 標準エラーレスポンス形式

**ユーザーストーリー:** クライアントとして、すべてのエラー
レスポンスが一貫した JSON 構造に従ってほしい。エラーを確実に
パースして処理できるようにするためである。

#### 受け入れ基準

1. THE Task_Service SHALL すべてのエラーレスポンスを
   Standard_Error_Format で返す: `{"error": "<説明的なメッセージ>"}`
2. THE Task_Service SHALL すべてのエラーレスポンスに
   `Content-Type: application/json` ヘッダーを含める
3. THE Task_Service SHALL 各エラーレスポンスの `error` フィールドに
   人間が読めるエラーの説明を提供する

### 要件 11: コンテナの構成

**ユーザーストーリー:** 開発者として、妥当なデフォルト値を備えた
Docker コンテナとしてアプリケーションをパッケージ化したい。
システムがポータブルで、任意のプラットフォームに簡単にデプロイ
できるようにするためである。

#### 受け入れ基準

1. THE Dockerfile SHALL API_Server と Task_Service を、必要なすべての
   依存関係とともに単一の Docker イメージにパッケージ化する
2. THE Dockerfile SHALL イメージサイズを最小化するため軽量な
   Python ベースイメージを使用する
3. THE Container SHALL API_Server が HTTP リクエストを受け付けるための
   設定可能なポート(デフォルトは 8000)を公開する
4. THE Container SHALL データベース接続設定とサーバーポートについて、
   環境変数による構成をサポートする
5. THE Dockerfile SHALL API_Server が稼働しているときに HTTP
   ステータス 200 を返すヘルスチェックエンドポイントを `/health` に含める

### 要件 12: データベースの移植性

**ユーザーストーリー:** 開発者として、アプリケーションに標準的な
リレーショナルデータベースを使ってほしい。デプロイ環境に合った
データベースバックエンドを選べるようにするためである。

#### 受け入れ基準

1. THE Task_Service SHALL SQL ベースのデータベースアクセスを使用し、
   Task_Store を SQLite、PostgreSQL、その他の SQL 互換データベースで
   支えられるようにする
2. THE Task_Service SHALL 外部データベース接続が構成されていない場合、
   Task_Store としてデフォルトで SQLite を使用する
3. WHEN 環境変数を通じて外部データベース接続文字列が提供される、
   THE Task_Service SHALL Task_Store として指定されたデータベースに
   接続する
4. THE Task_Service SHALL 起動時にテーブルが存在しない場合、必要な
   データベーステーブルを自動的に作成する

要件が揃ったら、Kiro はそれらを設計ドキュメントに、最終的にはタスクリストへとまとめ上げてくれます。各タスクを実行するたびに、TDD hook が Kiro に red-green-refactor サイクルでの作業を強制します。

例として、API の GET /tasks/{task_id} のルートハンドラーを構築するタスクを Kiro に実行させてみました。Kiro は、この機能をサポートするコードを書く前に、まず失敗するテストを書かなければならないと判断します。

Kiro はテストを構築し、それらが失敗することを確認します。これは red フェーズが正常に完了したことを意味します。次にコードを修正してテストを再実行し、テストがパスすることを確認します。これにより green フェーズが正常に完了したことが確認できます。新しいテストがパスしたら、Kiro は既存のすべてのテストを再実行し、それらが引き続き正しく動作することを検証します。

まとめ

長年の間、私は TDD と格闘してきました。コード品質の向上、バグの減少、リファクタリングへの自信といった利点を信じてはいましたが、現実は疲れるものでした。絶え間ないコンテキストスイッチ、red-green-refactor サイクルを守り続けるために必要な規律、そして実装の前にテストを書くことの苦痛が、TDD を有益なプラクティスというより負担に感じさせていました。

Kiro はこの状況を完全に変えてくれます。シンプルな hook を使うことで、TDD に必要な規律を自動化できました。hook があれば、Kiro はステップを飛ばしたり近道をしたりすることを許しません。私が意識しなくてもサイクルを守らせてくれます。コンテキストスイッチの負担、プロセスを守り続けるための規律、良いテストを書くことの単調さといった重荷を背負うことなく、TDD の恩恵を受けられるのです。本記事で示したように、モンティ・ホール問題のような単純なデモであっても、数十もの要件を持つ本番環境向けの REST API であっても、hook が TDD のプロセスを徹底してくれます。

私にとって、これこそが TDD のあるべき姿です。Kiro は、負担なしですべての恩恵を与えてくれます。

このアプローチをご自身で試してみたい方は、本記事の hook 設定をコピーして、ご自身の Kiro プロジェクトで使ってみてください。小さなプロジェクトから始めて、感触を確かめ、自分のやり方に合うように hook の指示を調整してみてください。これまで TDD で苦労してきた方や、コード品質を向上させる方法を探している方は、Kiro の hook を使った TDD をぜひ試してみてください。