はじめに
こんにちは。システム開発部 石橋です。
複数の Lambda・ECS・Batch を組み合わせたワークフローを Step Functions で構築した。いざテストしようとすると、「ステートマシン全体を通しで動かすしかないのか?」という壁にぶつかる。
この記事では、ステートマシンのテスト対象を 「配線」と「中身」に分離する 考え方と、それに基づくテスト戦略を紹介する。
ステートマシンは「薄いオーケストレーション層」
ワークフローの実ロジック――バリデーション、データ変換、計算処理――はすべて Lambda や ECS、Batch の中にある。ステートマシン自身が担うのは、それらを 正しい順序・条件で呼び出し、結果を次に渡す ことだ。
もちろん Step Functions も JSONata によるデータ整形や Choice による条件分岐を行う。ただ、これらはあくまで「配線の仕事」であり、ビジネスロジックそのものではない。
このように捉えると、1つの Task ステートのテスト対象は 2層に分かれる。
| 層 | 中身 | テスト手段 |
|---|---|---|
| アプリ側(invoke の中身) | Lambda/ECS/Batch が実行する処理 | 各コンポーネント自身の UT |
| ASL 側(その周りの配線) | 何を渡すか(Arguments)、結果をどう整形するか(Output)、分岐・Retry・Catch | TestState API |
この分離が、テスト戦略全体の起点になる。
テスト戦略の全体像
「何を検証するか」で、テストは 3層に整理できる。
| 層 | 検証対象 | ツール | 実行環境 |
|---|---|---|---|
| ① コンポーネント UT | 各 Lambda/ECS/Batch の内部ロジック | Go test, pytest 等 | ローカル / CI |
| ② ASL 配線テスト | Arguments/Output の JSONata 評価、Choice 分岐、Retry/Catch 遷移 | TestState API + mock | AWS(CI 可) |
| ③ E2E | 起動から完了までの全体フロー | 実環境 | ステージング / 本番相当 |
①は各コンポーネントの責務、③は最終的な結合確認で実環境でしかできない。この記事の主題は ②――ステートマシン固有の「配線」をどうテストするか だ。
TestState API で「配線」をテストする
背景:Step Functions Local の非サポート化
以前はローカルテスト用に Step Functions Local が提供されていたが、AWS 公式ドキュメントで "does not provide feature parity and is unsupported" と明記された。代わりに推奨されているのが TestState API だ。
2025年11月には TestState API に mock 対応が追加され、外部サービスを実際に呼ばずに配線だけを検証できるようになった。これにより、「配線」と「中身」の分離テストが実用的に行えるようになっている。
mock を付ける・付けないの判断
判断はシンプルで、そのステートが外部サービスを呼ぶかどうかで決まる。
| ステートタイプ | 外部呼び出し | mock | 理由 |
|---|---|---|---|
| Choice / Pass | なし | 不要 | ASL 内で完結する。仮定すべき外部の戻り値がない |
| Task(Lambda / ECS invoke) | あり | 付ける | 付けないと実サービスを呼んでしまう |
.sync / Map / Parallel |
あり | 必須 | mock なしだと TestState API 自体がエラーになる |
コード例:Lambda invoke の出力検証
たとえば、Lambda を呼んで件数を取得し、Output で結果を整形して次のステートに渡す Task を考える。
aws stepfunctions test-state \ --definition '{ "Type": "Task", "QueryLanguage": "JSONata", "Resource": "arn:aws:states:::lambda:invoke", "Arguments": { "FunctionName": "countItems", "Payload": "{% $states.input %}" }, "Output": "{% $states.result.Payload %}", "Next": "CheckCount" }' \ --input '{"process_id": "p1", "request_id": "r1"}' \ --mock '{"result": "{\"Payload\":{\"total_count\":5,\"chunk_count\":2}}"}' \ --inspection-level DEBUG
実行結果(抜粋):
{ "status": "SUCCEEDED", "output": "{\"total_count\":5,\"chunk_count\":2}", "nextState": "CheckCount" }
確認するのは以下の 2点だ。
output:Outputの JSONata 式が mock のPayloadから値を正しく取り出せているかnextState: 成功時の遷移先が期待通りか
total_count が 5 になるかどうかは Lambda 側の UT が担保する。mock に与えた 5 はあくまで「仮定」にすぎない。
mock 利用時は states:TestState 権限だけで済み、実行ロールや iam:PassRole は不要なため、CI への組み込みも容易だ。
TestState API の制約
- 単一ステートのみ実行。1回の呼び出しで検証できるのは 1ステートだけで、ステートマシン全体の起動(
StartExecution)はできない - AWS 側で実行される API であり、完全オフラインのローカルエミュレータではない。CI には AWS 認証情報とネットワーク接続が必要
まとめ
| 層 | 何を検証するか | どこで動くか | なぜ分けるか |
|---|---|---|---|
| ① コンポーネント UT | 各サービスの内部ロジック | ローカル / CI | ビジネスロジックは各コンポーネントの責務 |
| ② TestState API | ASL の配線(Arguments/Output/分岐/エラー遷移) | AWS(CI 可) | 配線の正しさはステートマシン固有の関心事 |
| ③ E2E | 起動〜完了の全体フロー | ステージング | 最終的な結合確認は実環境でしかできない |
ステートマシンを「薄いオーケストレーション層」と捉え、配線と中身を分けてテストする。この考え方を軸にすると、何を・どこで・なぜテストするかの判断が明快になる。
参考リンク
ENECHANGE で働きませんか
ENECHANGE では、AI 時代の開発体験を支える技術選定から一緒に取り組んでくれる仲間を募集しています。ぜひ採用情報をご覧ください。