Ruby
イベント
マガジン
技術ブログ
はじめに こんにちは、タイミーのプラットフォームエンジニアリングチームに所属している徳富( @yannKazu1 )です。 この記事では、スキマバイトサービス「タイミー」のバックエンド(Rails API)に Datadog Test Impact Analysis(TIA) を導入し、PRごとのCIフィードバックを大幅に高速化した取り組みについて紹介します。 なお、バックエンドはRailsアプリケーションなので、テストフレームワークは RSpec を前提として話を進めます。 サービスが成長してプロダクトの機能が増えてくると、テストも当然増えていきますよね。開発者が増えればPRの数も増え、CIの待ち時間がボトルネックになってくるのも、割とあるあるな課題だと思います。同じような悩みを抱えているチームの参考になれば嬉しいです。 背景:膨れ上がるテストとの戦い タイミーのバックエンドはモジュラーモノリスを採用しており、複数のインターフェースを1つのRailsアプリケーションで提供しています。 担当するバックエンドエンジニアの数もかなり多く、当然テストの数もそれに比例して増えていきます。 テスト数は約35,000。そして月に約2,000テストのペースで増加していました。 さらに最近はAIコーディングツールの活用も進み、テストが増えるペースが加速しています。人手に加えてAIコーディングツールの活用も進む中で、テストの増加速度が既存のチューニングでは吸収しきれない状況になりつつありました。 これまでの高速化の取り組み もちろん手をこまねいていたわけではありません。たとえば、 self-hosted runnerの活用 、GitHub Actionsのjobの 35並列 分割、 split-test による実行時間ベースの均等分割、ジョブ内のステップ並列実行(Redis/ESの起動とRuby/bundleセットアップをbackground + wait-allで同時進行)、 MySQLマイグレーションキャッシュ 、Dockerイメージキャッシュなど——泥臭いチューニングを積み重ねてきました。その結果、35,000テストを 約10分 で完走させるところまで持っていくことができました。 しかし、毎月2,000テストずつ増えていく状況では、並列数を増やし続けるのにも限界があります。ノード数を増やせばその分CIコストも増えていきますし、どこかで別のアプローチが必要になるのは明らかでした。 今後の成長を考えると、 すべてのPRですべてのテストを実行するのは持続可能ではない と判断しました。 Datadog Test Impact Analysis(TIA)という選択肢 そこで目をつけたのが Datadog Test Impact Analysis(TIA) です。 TIAは、コード変更の差分を解析し、その変更に関係のあるテストだけを選択的に実行する仕組みです。Datadogが各テストのコードカバレッジを収集・保持しており、「このファイルを変更したなら、このテストを実行すべき」という判断を自動で行ってくれます。 これにより、PRごとのCIでは 変更に関連するテストだけ を実行してフィードバックを高速化しつつ、品質の担保は別の仕組みで行うという戦略が取れるようになります。 TIAを使うための前提条件 TIAを使うには、まず Test Optimization を導入しておく必要があります。Test Optimizationはテスト結果やパフォーマンスデータをDatadogに送信して可視化する仕組みで、TIAはその上に乗っかる機能です。 具体的には以下が必要になります。 Test Optimization の設定が完了していること : datadog-ci gem( >= 1.0 )を導入し、CIからテスト結果をDatadogに送信できる状態にしておく Datadog側でTIAを有効化すること : Test Service Settingsページから、 Intelligent Test Runner Activation 権限を持つユーザーがTIAを有効にする必要がある 弊社ではもともとTest Optimizationを使ってテストの実行結果をDatadogで可視化していたので、TIAの導入自体はその延長線上でスムーズに進められました。 設計方針:GitHub Flow + マージキューを活かす ここからが今回のキモです。 弊社は基本的に GitHub Flow を採用しています。mainにマージされるとリリースが走るというシンプルな方針です。そして、mainへのマージには マージキュー(Merge Queue) を利用しています。マージキューの導入については、 こちらの記事 で詳しく紹介しています。 全体のフローはこんな感じです。 flowchart TD A["featureブランチで開発・push"] --> B["PR CI(ci_branch)<br/>TIA有効 → 関連テストのみ実行<br/>⚡ 高速フィードバック"] B -->|CI通過| C["レビュー & Approve"] C --> D["マージキューに投入"] D --> E["マージキュー CI(ci)<br/>全テスト実行<br/>+ Datadogカバレッジ収集"] E -->|全テスト通過| F["mainにマージ"] F --> G["リリース"] style B fill:#e8f5e9,stroke:#4caf50 style E fill:#fff3e0,stroke:#ff9800 ポイントは、 PRのCIとマージキューのCIで役割を分けている ところです。この「マージキュー」の特性をうまく活かすことで、以下のような構成を実現しました。 PRブランチ( ci_branch ワークフロー) TIAを有効化 して、差分に関連するテストのみを実行 開発者へのフィードバックを高速化 # ci_branch.yml jobs : ci : uses : ./.github/workflows/_ci.yml with : skip : false itr_enabled : true # TIAによるテストフィルタリングを有効化 secrets : inherit マージキュー( ci ワークフロー merge_group イベント) 全テストを実行 (TIAフィルタリングは無効) Datadogへのカバレッジ収集も同時に実施 これを通過しないとmainにマージされない # ci.yml on : merge_group : jobs : ci : uses : ./.github/workflows/_ci.yml with : skip : ${{ github.event_name != 'merge_group' }} # マージキューでは全テストを実行しつつカバレッジを収集 dd_coverage_enabled : true tia_test_skipping_mode : suite tia_force_run_all : true # gem側のスキップも無効化して全spec実行 secrets : inherit (コード内の tia_test_skipping_mode: suite は、テストの選定を「テストファイル単位」で行う設定です。なぜ suite にしたのかは、後述の「suiteモードを選んだ理由」で詳しく触れます。) つまり、 PRでは「速さ」を、マージキューでは「安全」を という役割分担です。 マージキューのテストを全件パスしないとmainにマージされず、mainにマージされないとリリースされない。この構造があるからこそ、PRのテストを絞っても品質を担保できるわけです。 カバレッジ収集はマージキューで TIAの精度を保つには、カバレッジデータを最新に保つ必要があります。 マージキューは mainにマージされる直前のコミットで全テストを実行 するので、ここでカバレッジを収集すれば常に最新の状態が保たれます。 # ci.yml(merge_group イベント時) dd_coverage_enabled : true # カバレッジ収集を有効化 tia_test_skipping_mode : suite # suite単位で収集 tia_force_run_all : true # gem側のスキップも無効化して全spec実行 当初はこのカバレッジ収集を別ワークフロー(Coverage Build)でも回していたのですが、マージキューで毎回「全テスト実行+カバレッジ収集」を行う構成にしたことで一本化でき、運用がシンプルになりました。 ddtest plan によるテスト選定と分割の工夫 ddtest plan の基本 TIAによるテスト選定には、Datadogが提供する ddtest CLIツールを使っています。 ./ddtest plan \ --platform ruby \ --framework rspec \ --min-parallelism 1 \ --max-parallelism 1 \ --test-skipping-mode suite \ --tests-location "**/*_spec.rb" \ --tests-exclude-pattern "vendor/**" ddtest plan はDatadog APIと通信して、現在のコミットの差分に対してスキップ可能なテストを判定し、実行すべきテストファイルの一覧を .testoptimization/runner/test-files.txt に出力します。 gem の環境変数スキップではなく ddtest plan を採用した理由 実は ddtest コマンドを使わなくても、datadog-ci gemを導入していれば、環境変数 DD_CIVISIBILITY_ITR_ENABLED=true の設定だけでTIAによるテストスキップを有効にできます。最も手軽な方法です。 ただ、弊社ではこの方法を採用しませんでした。理由は 35並列との相性が悪い からです。 gemの組み込みスキップは、RSpecの実行時に各テストケースを個別にスキップします。そのため、35ノードにテストを分割した後に各ノード内でスキップが走るので、「このノードは割り当てられたテストの大半がスキップされて30秒で終わったけど、別のノードは全部実行対象で5分かかった」といったことが起きます。結果としてノード間のばらつきが大きくなり、並列化の効率がガクッと落ちるんですよね。 そこで、テストの選定は ddtest plan で 実行前に 行い、選定されたファイルだけを split-test で均等に分割する、という方式を取りました。スキップの判断をRSpec実行の外に出すことで、各ノードに割り当てるテスト量を事前にコントロールできるようにしています。 ちなみに ddtest plan 自体にも --min-parallelism / --max-parallelism オプションによるノード分割機能があります。ただ、検証してみたところ split-test と比べてノード間の実行時間のばらつきが大きかったため、分割は引き続き split-test に任せる構成にしました。 ddtest plan は「どのテストを実行するか」の選定だけに使い、「どう分割するか」は split-test に委ねる、という役割分担です。 カバレッジ収集時の罠と DD_TIA_FORCE_RUN_ALL もう一つ、 DD_CIVISIBILITY_ITR_ENABLED 周りで地味にハマったポイントがあります。 TIAが正しくテストを選定するには、各テストがどのソースコードを通過するかという カバレッジデータ をDatadogに送る必要があります。カバレッジ収集は DD_CIVISIBILITY_ITR_ENABLED=true で有効化できます。ただし、この環境変数を true にすると、 カバレッジ収集と同時にテストのスキップも有効になります 。 つまり、カバレッジを集めたいだけなのに、TIAが「このテストはスキップしてOK」と判断したテストのカバレッジが収集できないという矛盾が起きます。これだとカバレッジデータに穴が空き、次回以降のTIA判定精度が落ちていきます。 この問題を解決するために、 datadog-ci gem が提供する datadog_itr_unskippable というRSpecメタデータを活用しています。 datadog-ci gem は、 DD_CIVISIBILITY_ITR_ENABLED=true のときにDatadog APIからスキップ可能なテストの一覧を取得し、該当するテストをスキップします。しかし、RSpecのメタデータに datadog_itr_unskippable: true が設定されているテストは、スキップ対象から除外されて必ず実行されます。 これを利用して、 spec_helper.rb で以下のように 全テストにunskippableメタデータを付与 しています。 # spec_helper.rb if ENV [ ' DD_TIA_FORCE_RUN_ALL ' ] == ' true ' # DD_CIVISIBILITY_ITR_ENABLED はカバレッジ収集と suite スキップを同時に有効化するため、 # 収集専用ジョブでは全 example を unskippable にして forced run させる RSpec .configure do |config| config.define_derived_metadata do |metadata| metadata[ :datadog_itr_unskippable ] = true end end end define_derived_metadata はRSpecの機能で、全テストのメタデータにデフォルト値を追加できます。これにより、gem側のスキップ判定が働いても「このテストはスキップ不可」と判定されるので、結果的に全テストが実行されます。 # マージキューでの環境変数設定 DD_CIVISIBILITY_ITR_ENABLED : true # カバレッジ収集を有効化 DD_TIA_FORCE_RUN_ALL : true # 全テストをunskippableにして必ず実行 DD_CIVISIBILITY_ITR_ENABLED=true でカバレッジ収集の仕組みを有効にしつつ、 DD_TIA_FORCE_RUN_ALL=true で全テストにunskippableメタデータを付与してスキップを防ぐ。これにより、全テストを実行して完全なカバレッジデータを収集できるようになります。 マージキューではこの組み合わせを使い、PRブランチでは DD_CIVISIBILITY_ITR_ENABLED=false (カバレッジ収集もスキップも無効)にして余計なオーバーヘッドを排除しています。 suiteモードを選んだ理由 TIAには testモード と suiteモード の2つの粒度があります。 testモード : 個々のテストケース( it ブロック)単位でスキップを判定 suiteモード : テストファイル( _spec.rb )単位でスキップを判定 今回は suiteモード を採用しました。 理由はシンプルで、 testモードでは35,000テストの分割判定に約2分かかるため です。差分が小さいPRなら問題ありません。しかし全テストに影響する変更(例えば spec_helper.rb の変更など)をした場合は、2分かけて「全テスト実行」という結論になり、かえってオーバーヘッドが大きくなってしまいます。その点、suiteモードなら対象がファイル単位なので、この判定が高速(5s程度)に完了します。 split-test との組み合わせ 前述の通り ddtest plan で選定したファイルを split-test で均等分割するのですが、ここが地味に苦労したポイントです。 split-test はファイルリストを直接受け取れず --tests-glob しか受け付けません。そこで、以下のようにシンボリックリンクを使ってglobのマッチ対象をTIA対象ファイルだけに制限するアプローチを取りました。 # TIA対象ファイルのシンボリックリンクを一時ディレクトリに作成 SPLIT_DIR=$(mktemp -d) while IFS= read -r f; do [ -f "$f" ] && mkdir -p "$SPLIT_DIR/$(dirname "$f")" && ln -s "$WORKSPACE/$f" "$SPLIT_DIR/$f" done < .testoptimization/runner/test-files.txt # 一時ディレクトリ内で split-test を実行(TIA対象のみが均等に分割される) (cd "$SPLIT_DIR" && "$WORKSPACE/split-test" \ --junit-xml-report-dir "$WORKSPACE/tmp/rspec_junit" \ --node-index $NODE_INDEX \ --node-total 35 \ --tests-glob "spec/**/*_spec.rb" \ --tests-glob "packs/*/spec/**/*_spec.rb") ちょっとトリッキーですが、これにより TIAで絞り込んだテストを、実行時間ベースで均等に35分割する ことが実現できました。 なお、35並列のうちTIAの絞り込みで対象テストがゼロになるノードも出てきます。その場合は空のダミーJUnitレポートを出力しておき、後続のジョブが正常に動くようにしています。 PRの全変更を正しく評価するための工夫 もう一つ、導入時にハマったポイントがあります。 ddtest はHEADコミットのdiffだけを見てテスト選定を行います。PRに複数コミットがある場合、最後のコミットの変更しか考慮されません。これだと初期コミットで変更したファイルに関連するテストがスキップされてしまう可能性があります。 そこで、GitHub APIで merge-base(分岐点) を取得し、PRの全変更を1つのdiffとして ddtest に認識させる仕組みを入れています。 # PRの分岐点を取得 MERGE_BASE=$(gh api "repos/$GITHUB_REPOSITORY/compare/main...$GITHUB_SHA" \ --jq '.merge_base_commit.sha') # 分岐点を親、現在のツリーを内容とするコミットを作成 git fetch --depth=1 origin "$MERGE_BASE" --no-tags TREE=$(git rev-parse "HEAD^{tree}") TIA_COMMIT=$(git commit-tree "$TREE" -p "$MERGE_BASE" -m "TIA: all PR changes") git reset --soft "$TIA_COMMIT" これにより、HEADのdiffが「PR全体の変更」を表すようになり、TIAが正しく全変更を評価できるようになります。 全体のアーキテクチャまとめ 最終的な構成を図にすると以下のようになります。 flowchart TD A[開発者がPRを作成] --> B subgraph PR["ci_branch ワークフロー(PRブランチ)"] B["ddtest plan (TIA)\n関連テストのみ抽出"] --> C["split-test\n35並列に均等分割"] --> D["RSpec実行\n(関連テストのみ)"] end D -->|テスト通過| E[マージキューに投入] E --> F subgraph MQ["ci ワークフロー(merge_group イベント)"] F["全テスト実行\n(TIAフィルタリングなし)"] --> G["Datadogカバレッジ収集\n(TIA精度を維持)"] end G -->|全テスト通過| H["mainにマージ → リリース"] style PR fill:#e8f5e9,stroke:#4caf50 style MQ fill:#fff3e0,stroke:#ff9800 導入時のハマりポイントまとめ 実際に導入してみて、いくつかハマったポイントをまとめておきます。 1. testモードの分割コスト 前述の通り、testモードでは35,000テストの分割判定に約2分かかります。suiteモードなら数秒で完了します。テスト数が多い場合はsuiteモード一択だと思います。 2. split-test との組み合わせ split-test がファイルリストを直接受け取れないため、シンボリックリンクを使った一時ディレクトリ方式を採用しました。ちょっとトリッキーですが、確実に動きます。 3. マルチコミットPRの差分評価 ddtest がHEADコミットのdiffしか見ない問題は、merge-baseからのコミットを作り直すことで解決しました。 4. カバレッジの鮮度 TIAの精度はカバレッジデータの鮮度に依存します。当初は別ワークフロー(Coverage Build)でも収集していましたが、マージキューで毎回全テスト+カバレッジ収集を行う構成にしたことで一本化でき、シンプルになりました。 5. TIAで全テストがスキップされるノードの対策 35並列のうち、TIAで対象テストがゼロになるノードが出てきます。その場合はダミーのJUnitレポートを出力して後続のジョブが正常に動くようにしています。 成果 TIA導入前は、PRのCIで全テストを実行していたため 約10分 かかっていました。TIA導入後は変更の差分によって実行されるテスト数が変わりますが、差分が小さいPRでは 1〜2分 で完了するケースもあり、大幅に高速化しています。 おわりに Datadog TIAの導入により、PRごとのテスト実行時間を大幅に短縮しつつ、マージキューで全テストを実行するという安全な構成を実現できました。 ポイントをまとめると: PRブランチ : TIAで関連テストのみ実行 → 高速フィードバック マージキュー : 全テスト実行 + カバレッジ収集 → 品質担保 suiteモード : テスト数が多い場合はtestモードよりsuiteモードが実用的 ddtest plan + split-test : TIAフィルタリングと均等分割の組み合わせがキモ GitHub Flow + マージキュー : この組み合わせがTIA導入の前提条件として非常にフィットした テスト数がどんどん増えていく中で、「全部回す」から「必要なものだけ回す」へのシフトは避けて通れない道だと思います。同じような課題を抱えているチームの参考になれば幸いです。
本記事は 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)の概要 テスト駆動開発の基本的な考え方は次のとおりです。 構築する新機能ごとに、その新機能の振る舞いを特定します。 新しい振る舞いそれぞれに対して、もしその振る舞いが存在すればパスするはずのテストを書きます。 テストを実行します。振る舞いがまだ存在しないため、すべてのテストは失敗するはずです。 新しいテストをパスさせるもっともシンプルなコードを書きます。 すべてのテストがパスし続けることを確認しながら、必要に応じてリファクタリングします。 これが 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 を選択します。表示されたウィンドウで、以下を入力します。 Title : TDD: Test First Description : 本番コードを書く前に、失敗するテストを書くよう促すことでテスト駆動開発を徹底します。red/green/refactor サイクルに従います。すなわち、失敗するテストを書き(red)、それをパスさせる最小限のコードを書き(green)、その後リファクタリングします。 Event : Pre Tool Use Tool name : write Action : Ask 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 をぜひ試してみてください。
こんにちは!DevRelの重田( @Shige0096 )です。 メドレーでは夏企画として『MEDLEY Summer Tech Blog Relay』と題して、ブログリレーを開催します! 7/13(月)〜8/21(金)まで毎日異なるメンバーが技術やエンジニアリング、個人開発など幅広いテーマでテックブログを公開していきます! 本記事にて毎日ブログを追記更新していくので、ぜひお楽しみください✨ ※土日祝を除く 👇よろしければ昨年の記事もぜひチェックしてみてください! MEDLEY Summer Tech Blog Relay | MEDLEY Developer Portal こんにちは!メドレーでDevRelをしている重田です。 今年も暑い日が続いていますがいかがお過ごしですか? メドレーでは夏企画として『MEDLEY Summer Tech Blog Relay』と題して、ブログリレーを開催します! 8/2... developer.medley.jp 🌻 ブログリレーカレンダー 🗓️第1週(7/13~7/17) Day1:(仮)🍄(玉井) Day2:Fivetranについて(本多) Day3:TBD(森川) Day4:BigQueryのコスト削減周り(林田) Day5:TBD(宋) 🗓️第2週(7/21~7/24) Day6:FY25 CLINICS大規模障害対応で認証基盤の刷新をした時に得た学びについて(山田) Day7:ローカルLLMの個人での活用について(斎藤) Day8:自作ハーネスとローカルLLMをゴリゴリ(福島) Day9:TBD(稲村) 🗓️第3週(7/27~7/31) Day10:Rubyで組み込み・ゲーム・AIのどれか!(藤原) Day11:TBD(平林) Day12:TBD(高橋) Day13:(仮) AI API 叩くとき考えること(山下) Day14:Jetson Orin Nano Super によるローカルMLLM活用について(山本) 🗓️第4週(8/3~8/7) Day15:Maestro E2E(菅原) Day16:TBD(奥澤) Day17:TBD(佐藤) Day18:TBD(牧) Day19:TBD(久保) 🗓️第5週(8/10~8/14) Day20:TBD(柏木) Day21:TBD(山本) Day22:TBD(村上) Day23:TBD(竹本) 🗓️第6週(8/17~8/21) Day24:TBD(山河) Day25:TBD(亀澤) Day26:TBD(清水) Day27:メドレーにおける0→1開発のリアルな話(エージェント業務システム)(德永) Day28:TBD(倉林) 🍉 We’re hiring! メドレーでは、「医療ヘルスケアの未来をつくる」仲間を大募集しています! 少しでも興味をお持ちいただけましたら、ぜひ、カジュアル面談にお越しください🙌 ご応募お待ちしております!! メドレーで働く|株式会社メドレー メドレーでの働き方や人事制度、求人情報など、採用に関する情報をご紹介します。 www.medley.jp
動画
該当するコンテンツが見つかりませんでした














