AWS Step Functions の強化された TestState API でワークフローをローカルテストしてみた
はじめに
2025年11月、AWS Step Functions の TestState API が大幅に強化されました。モックによるサービス統合のシミュレーション、Map/Parallel ステートのサポート、リトライ機構のテストなどが追加されています。
今回の強化で変わった点を整理します。
| 従来 | 強化後 |
|---|---|
| モックなし(実サービス呼び出しのみ) | モック対応(result / errorOutput + fieldValidationMode) |
| Task/Pass/Wait/Choice/Succeed/Fail のみ | Map/Parallel を含む全ステートタイプ、および Activity・.sync・.waitForTaskToken パターンに対応(モック指定時) |
| 単一ステート定義のみ指定可能 | stateName パラメータで完全なステートマシン定義から個別テスト |
| context/variables 指定不可 | --context / --variables で実行コンテキストと変数を注入可能 |
| リトライテスト不可 | stateConfiguration でリトライ・Map 失敗シミュレーション |
| inspectionData にエラー詳細なし | errorDetails(catchIndex, retryIndex, retryBackoffIntervalSeconds)追加 |
TestState API の利用は無料で、Step Functions の対応リージョンで利用できます。
検証内容
検証環境
| 項目 | 値 |
|---|---|
| AWS CLI | v2(最新版) |
| リージョン | ap-northeast-1 |
| IAM | CLI 実行主体に states:TestState 権限あり |
モック使用時はステート実行ロール(roleArn)の指定は不要です。
モックによる Task ステートのテスト(成功レスポンス)
Lambda invoke の Task ステートに対して、--mock パラメータでサービス呼び出しの結果をモックします。--inspection-level DEBUG を指定することで、入出力変換の各段階を確認できます。
aws stepfunctions test-state --region ap-northeast-1 \
--definition '{
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {"FunctionName": "process-order", "Payload.$": "$"},
"ResultPath": "$.lambdaResult",
"End": true
}' \
--input '{"orderId": "order-001", "amount": 150.75}' \
--inspection-level DEBUG \
--mock '{"fieldValidationMode": "NONE", "result": "{\"Payload\": {\"statusCode\": 200, \"body\": {\"orderId\": \"order-001\", \"status\": \"processed\"}}}"}'
{
"output": "{\"orderId\":\"order-001\",\"amount\":150.75,\"lambdaResult\":{\"Payload\":{\"statusCode\":200,\"body\":{\"orderId\":\"order-001\",\"status\":\"processed\"}}}}",
"inspectionData": {
"input": "{\"orderId\": \"order-001\", \"amount\": 150.75}",
"afterInputPath": "{\"orderId\": \"order-001\", \"amount\": 150.75}",
"afterParameters": "{\"FunctionName\":\"process-order\",\"Payload\":{\"orderId\":\"order-001\",\"amount\":150.75}}",
"result": "{\"Payload\": {\"statusCode\": 200, \"body\": {\"orderId\": \"order-001\", \"status\": \"processed\"}}}",
"afterResultSelector": "{\"Payload\": {\"statusCode\": 200, \"body\": {\"orderId\": \"order-001\", \"status\": \"processed\"}}}",
"afterResultPath": "{\"orderId\":\"order-001\",\"amount\":150.75,\"lambdaResult\":{\"Payload\":{\"statusCode\":200,\"body\":{\"orderId\":\"order-001\",\"status\":\"processed\"}}}}"
},
"status": "SUCCEEDED"
}
inspectionData により、入力から最終出力に至るデータ変換の各段階を追跡できます。
モックによるエラーシミュレーション
--mock の errorOutput を使ってサービス呼び出しの失敗をシミュレートし、Catch ハンドラの動作を確認します。
aws stepfunctions test-state --region ap-northeast-1 \
--definition '{
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {"FunctionName": "process-order"},
"Catch": [
{"ErrorEquals": ["Lambda.ServiceException"], "ResultPath": "$.error", "Next": "HandleServiceError"},
{"ErrorEquals": ["States.ALL"], "ResultPath": "$.error", "Next": "HandleGenericError"}
],
"Next": "SuccessState"
}' \
--input '{"orderId": "order-002"}' \
--inspection-level DEBUG \
--mock '{"errorOutput": {"cause": "Service is unavailable", "error": "Lambda.ServiceException"}}'
{
"output": "{\"orderId\":\"order-002\",\"error\":{\"Error\":\"Lambda.ServiceException\",\"Cause\":\"Service is unavailable\"}}",
"error": "Lambda.ServiceException",
"cause": "Service is unavailable",
"inspectionData": {
"input": "{\"orderId\": \"order-002\"}",
"afterInputPath": "{\"orderId\": \"order-002\"}",
"afterParameters": "{\"FunctionName\":\"process-order\"}",
"afterResultPath": "{\"orderId\":\"order-002\",\"error\":{\"Error\":\"Lambda.ServiceException\",\"Cause\":\"Service is unavailable\"}}",
"errorDetails": {
"catchIndex": 0
}
},
"nextState": "HandleServiceError",
"status": "CAUGHT_ERROR"
}
status が CAUGHT_ERROR となり、nextState は 1 つ目の Catch ハンドラで指定した HandleServiceError を示しています。errorDetails.catchIndex が 0 であることから、どの Catch ハンドラが捕捉したかも確認できます。output には ResultPath で指定した $.error にエラー情報がマージされています。
fieldValidationMode の動作確認
モックレスポンスのバリデーション強度を fieldValidationMode で制御できます。同一の不正なモック(数値フィールド StatusCode に文字列を渡す)を各モードで実行し、動作の違いを確認します。
STRICT モード(デフォルト):
aws stepfunctions test-state --region ap-northeast-1 \
--definition '{"Type": "Task", "Resource": "arn:aws:states:::lambda:invoke", "Parameters": {"FunctionName": "my-func"}, "End": true}' \
--input '{}' \
--mock '{"fieldValidationMode": "STRICT", "result": "{\"StatusCode\": \"not-a-number\"}"}'
An error occurred (ValidationException) when calling the TestState operation:
Mock result schema validation error: Field 'StatusCode' must be a number
PRESENT モード:
aws stepfunctions test-state --region ap-northeast-1 \
--definition '{"Type": "Task", "Resource": "arn:aws:states:::lambda:invoke", "Parameters": {"FunctionName": "my-func"}, "End": true}' \
--input '{}' \
--mock '{"fieldValidationMode": "PRESENT", "result": "{\"StatusCode\": \"not-a-number\"}"}'
STRICT と同じ ValidationException が発生しました。PRESENT モードでもフィールドが存在する場合は型検証が行われます。
NONE モード:
aws stepfunctions test-state --region ap-northeast-1 \
--definition '{"Type": "Task", "Resource": "arn:aws:states:::lambda:invoke", "Parameters": {"FunctionName": "my-func"}, "End": true}' \
--input '{}' \
--mock '{"fieldValidationMode": "NONE", "result": "{\"StatusCode\": \"not-a-number\"}"}'
{
"output": "{\"StatusCode\": \"not-a-number\"}",
"status": "SUCCEEDED"
}
NONE では API モデルに基づくモック結果のフィールド検証が行われず、不正な型の値でも成功します。
API モデルに未定義のフィールドのみの場合(STRICT):
--mock '{"fieldValidationMode": "STRICT", "result": "{\"InvalidField\": \"bad-data\"}"}'
{
"output": "{\"InvalidField\": \"bad-data\"}",
"status": "SUCCEEDED"
}
これは STRICT でも成功します。fieldValidationMode が検証するのは API モデルに定義された既知フィールドのみで、未定義フィールドは無視されます。
まとめると、今回確認できた範囲とドキュメント上の仕様差は以下の通りです。
| モード | 既知フィールドの型検証 | 必須フィールドの存在チェック | API モデル未定義フィールド |
|---|---|---|---|
| STRICT | する | する(ドキュメントベース、今回未検証) | 検証対象外 |
| PRESENT | する(存在する場合) | しない(ドキュメントベース、今回未検証) | 検証対象外 |
| NONE | しない | しない | 検証対象外 |
「検証対象外」はモック結果から当該フィールドが除去される意味ではなく、API モデルに対するスキーマ検証の対象外であることを示します。
リトライ機構のテスト(retrierRetryCount)
--state-configuration の retrierRetryCount パラメータを使うと、「すでに何回リトライ済みか」を指定した状態で、次のリトライ可否をシミュレートできます。
テスト対象のステート定義:
{
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {"FunctionName": "my-func"},
"Retry": [{
"ErrorEquals": ["Lambda.TooManyRequestsException"],
"IntervalSeconds": 2,
"MaxAttempts": 3,
"BackoffRate": 2.0
}],
"Catch": [{"ErrorEquals": ["States.ALL"], "ResultPath": "$.error", "Next": "HandleError"}],
"Next": "SuccessState"
}
retrierRetryCount を 0 から 3 まで変えて実行した結果です。
| retrierRetryCount | status | retryBackoffIntervalSeconds | 計算式 |
|---|---|---|---|
| 0 | RETRIABLE | 2 | 2 × 2^0 = 2 |
| 1 | RETRIABLE | 4 | 2 × 2^1 = 4 |
| 2 | RETRIABLE | 8 | 2 × 2^2 = 8 |
| 3(= MaxAttempts) | CAUGHT_ERROR | - | リトライ枯渇 → Catch |
retrierRetryCount=3(MaxAttempts に到達)のレスポンス:
{
"output": "{\"orderId\":\"order-003\",\"error\":{\"Error\":\"Lambda.TooManyRequestsException\",\"Cause\":\"Rate exceeded\"}}",
"error": "Lambda.TooManyRequestsException",
"cause": "Rate exceeded",
"inspectionData": {
"errorDetails": {
"catchIndex": 0,
"retryIndex": 0
}
},
"nextState": "HandleError",
"status": "CAUGHT_ERROR"
}
retryBackoffIntervalSeconds は次のリトライまでの待ち時間で、この定義では IntervalSeconds × BackoffRate^retrierRetryCount に一致しています(MaxDelaySeconds 指定時は上限が適用されます)。MaxAttempts に達するとリトライが枯渇し、Catch ハンドラに遷移します。
retryIndex はどの Retry 定義(配列のインデックス)が適用されたかを表します。
Map ステートのテスト
--inspection-level DEBUG を指定すると、ItemsPath による配列抽出や ItemSelector の変換過程を inspectionData で確認できます。Map ステートのモックでは ItemProcessor 内は実行されず、result がステート全体の出力として扱われます。
aws stepfunctions test-state --region ap-northeast-1 \
--definition '{
"Type": "Map",
"ItemsPath": "$.items",
"ItemSelector": {"value.$": "$$.Map.Item.Value", "index.$": "$$.Map.Item.Index"},
"ItemProcessor": {
"ProcessorConfig": {"Mode": "INLINE"},
"StartAt": "ProcessItem",
"States": {"ProcessItem": {"Type": "Task", "Resource": "arn:aws:states:::lambda:invoke", "Parameters": {"FunctionName": "process-item"}, "End": true}}
},
"MaxConcurrency": 5,
"Next": "NextStep"
}' \
--input '{"items": [{"id": 1, "name": "item-a"}, {"id": 2, "name": "item-b"}, {"id": 3, "name": "item-c"}]}' \
--inspection-level DEBUG \
--mock '{"fieldValidationMode": "NONE", "result": "[{\"processed\": true, \"id\": 1}, {\"processed\": true, \"id\": 2}, {\"processed\": true, \"id\": 3}]"}'
レスポンス全文
{
"output": "[{\"processed\":true,\"id\":1},{\"processed\":true,\"id\":2},{\"processed\":true,\"id\":3}]",
"inspectionData": {
"input": "{\"items\": [{\"id\": 1, \"name\": \"item-a\"}, {\"id\": 2, \"name\": \"item-b\"}, {\"id\": 3, \"name\": \"item-c\"}]}",
"afterInputPath": "{\"items\": [{\"id\": 1, \"name\": \"item-a\"}, {\"id\": 2, \"name\": \"item-b\"}, {\"id\": 3, \"name\": \"item-c\"}]}",
"afterItemsPath": "[{\"id\":1,\"name\":\"item-a\"}, {\"id\":2,\"name\":\"item-b\"}, {\"id\":3,\"name\":\"item-c\"}]",
"afterItemSelector": "[{\"index\":0,\"value\":{\"id\":1,\"name\":\"item-a\"}}, {\"index\":1,\"value\":{\"id\":2,\"name\":\"item-b\"}}, {\"index\":2,\"value\":{\"id\":3,\"name\":\"item-c\"}}]",
"maxConcurrency": 5
},
"nextState": "NextStep",
"status": "SUCCEEDED"
}
afterItemsPath で ItemsPath による配列抽出結果、afterItemSelector で各アイテムに index/value が付与された変換結果を確認できます。
Map ステートの失敗許容閾値テスト(mapIterationFailureCount)
mapIterationFailureCount は、Distributed Map の反復失敗数をシミュレートするためのパラメータです。Mode: "DISTRIBUTED" の Map ステートに対して、失敗許容閾値を検証します。以下の定義を map-definition.json に保存して使用します。
{
"Type": "Map",
"ItemsPath": "$.items",
"ItemSelector": {"value.$": "$$.Map.Item.Value", "index.$": "$$.Map.Item.Index"},
"ItemProcessor": {
"ProcessorConfig": {"Mode": "DISTRIBUTED"},
"StartAt": "ProcessItem",
"States": {"ProcessItem": {"Type": "Task", "Resource": "arn:aws:states:::lambda:invoke", "Parameters": {"FunctionName": "process-item"}, "End": true}}
},
"ToleratedFailureCount": 2,
"Catch": [
{"ErrorEquals": ["States.ExceedToleratedFailureThreshold"], "ResultPath": "$.error", "Next": "HandleMapFailure"}
],
"Next": "NextStep"
}
mapIterationFailureCount=3(閾値超過):
aws stepfunctions test-state --region ap-northeast-1 \
--definition file://map-definition.json \
--input '{"items": [{"id": 1}, {"id": 2}, {"id": 3}]}' \
--inspection-level DEBUG \
--mock '{"fieldValidationMode": "NONE", "result": "[{\"id\": 1}, {\"id\": 2}, {\"id\": 3}]"}' \
--state-configuration '{"mapIterationFailureCount": 3}'
{
"error": "States.ExceedToleratedFailureThreshold",
"cause": "The specified tolerated failure threshold was exceeded",
"inspectionData": {
"errorDetails": {"catchIndex": 0},
"toleratedFailureCount": 2
},
"nextState": "HandleMapFailure",
"status": "CAUGHT_ERROR"
}
mapIterationFailureCount=2(閾値ちょうど):
{
"nextState": "NextStep",
"status": "SUCCEEDED"
}
ToleratedFailureCount を超過したときのみ States.ExceedToleratedFailureThreshold が発生し、ちょうど同じ値(2 = 2)では成功として扱われます。なお、Task ステートのエラー時とは異なり、Map の失敗許容閾値エラーでは output フィールドが返されませんでした。
Parallel ステートのテスト
Parallel ステートのモックでは、result にブランチ数と同じ要素数の配列を渡します。
aws stepfunctions test-state --region ap-northeast-1 \
--definition '{
"Type": "Parallel",
"Branches": [
{"StartAt": "ProcessPayment", "States": {"ProcessPayment": {"Type": "Task", "Resource": "arn:aws:states:::lambda:invoke", "Parameters": {"FunctionName": "process-payment"}, "End": true}}},
{"StartAt": "UpdateInventory", "States": {"UpdateInventory": {"Type": "Task", "Resource": "arn:aws:states:::lambda:invoke", "Parameters": {"FunctionName": "update-inventory"}, "End": true}}}
],
"ResultPath": "$.parallelResults",
"Next": "FinalStep"
}' \
--input '{"orderId": "order-004", "amount": 99.99}' \
--inspection-level DEBUG \
--mock '{"fieldValidationMode": "NONE", "result": "[{\"paymentId\": \"pay-001\", \"status\": \"completed\"}, {\"inventoryUpdated\": true, \"itemsReserved\": 3}]"}'
{
"output": "{\"orderId\":\"order-004\",\"amount\":99.99,\"parallelResults\":[{\"paymentId\":\"pay-001\",\"status\":\"completed\"},{\"inventoryUpdated\":true,\"itemsReserved\":3}]}",
"inspectionData": {
"afterResultSelector": "[{\"paymentId\": \"pay-001\", \"status\": \"completed\"}, {\"inventoryUpdated\": true, \"itemsReserved\": 3}]",
"afterResultPath": "{\"orderId\":\"order-004\",\"amount\":99.99,\"parallelResults\":[{\"paymentId\":\"pay-001\",\"status\":\"completed\"},{\"inventoryUpdated\":true,\"itemsReserved\":3}]}"
},
"nextState": "FinalStep",
"status": "SUCCEEDED"
}
--mock で渡した配列の各要素が各ブランチの結果として扱われ、ResultPath で元の入力にマージされています。
stateName パラメータによる個別ステートテスト
--state-name パラメータを使うと、完全なステートマシン定義を --definition に渡した上で、特定のステートだけを実行できます。前のステートの出力を次のステートの入力として渡すことで、チェーン実行のシミュレーションも可能です。
以下のステートマシン定義を definition.json に保存して使用します。
{
"StartAt": "ValidateOrder",
"States": {
"ValidateOrder": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {"FunctionName": "validate-order", "Payload.$": "$"},
"ResultPath": "$.validation",
"Next": "CheckValidation"
},
"CheckValidation": {
"Type": "Choice",
"Choices": [{"Variable": "$.validation.Payload.isValid", "BooleanEquals": true, "Next": "ProcessOrder"}],
"Default": "RejectOrder"
},
"ProcessOrder": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {"FunctionName": "process-order"},
"End": true
},
"RejectOrder": {
"Type": "Fail",
"Error": "ValidationFailed",
"Cause": "Order validation failed"
}
}
}
ステップ1: ValidateOrder を実行
--state-name ValidateOrder を指定し、モックで検証成功のレスポンスを返します。
aws stepfunctions test-state --region ap-northeast-1 \
--definition file://definition.json \
--state-name ValidateOrder \
--input '{"orderId": "order-005", "amount": 200}' \
--inspection-level DEBUG \
--mock '{"fieldValidationMode": "NONE", "result": "{\"Payload\": {\"isValid\": true, \"orderId\": \"order-005\"}}"}'
結果の nextState は CheckValidation を示し、output には $.validation にモック結果がマージされています。
ステップ2: CheckValidation を実行
ステップ1 の output をそのまま --input に渡し、--state-name CheckValidation を指定します。Choice ステートはモック不要で条件評価が行われます。
aws stepfunctions test-state --region ap-northeast-1 \
--definition file://definition.json \
--state-name CheckValidation \
--input '{"orderId": "order-005", "amount": 200, "validation": {"Payload": {"isValid": true, "orderId": "order-005"}}}' \
--inspection-level DEBUG
{
"output": "{\"orderId\":\"order-005\",\"amount\":200,\"validation\":{\"Payload\":{\"isValid\":true,\"orderId\":\"order-005\"}}}",
"nextState": "ProcessOrder",
"status": "SUCCEEDED"
}
$.validation.Payload.isValid が true のため、nextState は ProcessOrder を返しました。
output を次のテストの --input に渡し、nextState を次の --state-name に指定することで、ステート間の遷移をチェーン的にテストできます。
context パラメータの利用(waitForTaskToken パターン)
--context パラメータで $$.Task.Token や $$.Execution.Id などの実行コンテキスト値を注入できます。
aws stepfunctions test-state --region ap-northeast-1 \
--definition '{
"Type": "Task",
"Resource": "arn:aws:states:::sqs:sendMessage.waitForTaskToken",
"Parameters": {
"QueueUrl": "https://sqs.ap-northeast-1.amazonaws.com/123456789012/approval-queue",
"MessageBody": {
"taskToken.$": "$$.Task.Token",
"orderId.$": "$.orderId",
"executionId.$": "$$.Execution.Id",
"stateName.$": "$$.State.Name"
}
},
"Next": "CheckApproval"
}' \
--input '{"orderId": "order-006", "amount": 500}' \
--inspection-level DEBUG \
--context '{"Task": {"Token": "AAAAKgAAAAIAAAAAAAAAAQ=="}, "Execution": {"Id": "arn:aws:states:ap-northeast-1:123456789012:execution:OrderWorkflow:exec-001", "Name": "exec-001", "StartTime": "2026-07-09T10:00:00Z"}, "State": {"Name": "WaitForApproval", "EnteredTime": "2026-07-09T10:00:05Z"}}' \
--mock '{"fieldValidationMode": "NONE", "result": "{\"approved\": true, \"approver\": \"admin@example.com\"}"}'
inspectionData.afterParameters の抜粋:
{
"QueueUrl": "https://sqs.ap-northeast-1.amazonaws.com/123456789012/approval-queue",
"MessageBody": {
"executionId": "arn:aws:states:ap-northeast-1:123456789012:execution:OrderWorkflow:exec-001",
"orderId": "order-006",
"stateName": "WaitForApproval",
"taskToken": "AAAAKgAAAAIAAAAAAAAAAQ=="
}
}
$$.Task.Token、$$.Execution.Id、$$.State.Name がそれぞれ --context で渡した値に正しく解決されています。
variables パラメータの利用
JSONata クエリ言語を使用するステートでは、--variables パラメータでワークフロー変数を注入してテストできます。
aws stepfunctions test-state --region ap-northeast-1 \
--definition '{
"QueryLanguage": "JSONata",
"Type": "Pass",
"Output": {
"orderId": "{% $states.input.orderId %}",
"customerTier": "{% $customerTier %}",
"discountRate": "{% $discountRate %}",
"finalAmount": "{% $states.input.amount * (1 - $discountRate) %}"
},
"Next": "Done"
}' \
--input '{"orderId": "order-007", "amount": 300}' \
--inspection-level DEBUG \
--variables '{"customerTier": "premium", "discountRate": 0.15}'
{
"output": "{\"orderId\":\"order-007\",\"customerTier\":\"premium\",\"discountRate\":0.15,\"finalAmount\":255}",
"inspectionData": {
"input": "{\"orderId\": \"order-007\", \"amount\": 300}",
"variables": "{\"discountRate\":0.15,\"customerTier\":\"premium\"}"
},
"nextState": "Done",
"status": "SUCCEEDED"
}
--variables で渡した customerTier と discountRate が JSONata 式で正しく評価されています。finalAmount は 300 × (1 - 0.15) = 255 と計算されました。
まとめ
強化された TestState API により、Step Functions のステート単位テストで確認できる範囲が大きく広がりました。今回の検証では、モックによる Task ステートの成功・失敗レスポンス、Catch / Retry の分岐、Map/Parallel ステートの入出力変換を確認しました。さらに実行コンテキストや JSONata 変数の注入も AWS CLI から検証しています。
--mock と --state-name を組み合わせることで、特定の実行経路におけるデータ受け渡しと遷移先を段階的に検証できます。
なお、モックを使った Map/Parallel の検証では内部の ItemProcessor や Branch 内のステートを実行するのではなく、モック結果をステート全体の結果として扱います。そのため、内部処理そのものよりも、入出力変換や期待する出力形状の確認に向いています。
ステート単位の検証や特定経路の確認を自動化したい場面で、有力な選択肢になりそうです。
参考リンク









