AWS Step Functions の強化された TestState API でワークフローをローカルテストしてみた

AWS Step Functions の強化された TestState API でワークフローをローカルテストしてみた

AWS Step Functions の TestState API が2025年11月に強化され、モック・リトライ・Map/Parallel・変数注入をデプロイなしでテスト可能になりました。AWS CLI で各新機能を検証し、ユニットテストとしての実用性を確認します。
2026.07.11

はじめに

2025年11月、AWS Step Functions の TestState API が大幅に強化されました。モックによるサービス統合のシミュレーション、Map/Parallel ステートのサポート、リトライ機構のテストなどが追加されています。

https://aws.amazon.com/about-aws/whats-new/2025/11/aws-step-functions-local-testing-teststate-api/

https://aws.amazon.com/blogs/compute/testing-step-functions-workflows-a-guide-to-the-enhanced-teststate-api/

今回の強化で変わった点を整理します。

従来 強化後
モックなし(実サービス呼び出しのみ) モック対応(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 の対応リージョンで利用できます。

https://docs.aws.amazon.com/step-functions/latest/dg/test-state-isolation.html

検証内容

検証環境

項目
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 により、入力から最終出力に至るデータ変換の各段階を追跡できます。

モックによるエラーシミュレーション

--mockerrorOutput を使ってサービス呼び出しの失敗をシミュレートし、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"
}

statusCAUGHT_ERROR となり、nextState は 1 つ目の Catch ハンドラで指定した HandleServiceError を示しています。errorDetails.catchIndex0 であることから、どの 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-configurationretrierRetryCount パラメータを使うと、「すでに何回リトライ済みか」を指定した状態で、次のリトライ可否をシミュレートできます。

テスト対象のステート定義:

{
  "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\"}}"}'

結果の nextStateCheckValidation を示し、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.isValidtrue のため、nextStateProcessOrder を返しました。

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 で渡した customerTierdiscountRate が JSONata 式で正しく評価されています。finalAmount300 × (1 - 0.15) = 255 と計算されました。

まとめ

強化された TestState API により、Step Functions のステート単位テストで確認できる範囲が大きく広がりました。今回の検証では、モックによる Task ステートの成功・失敗レスポンス、Catch / Retry の分岐、Map/Parallel ステートの入出力変換を確認しました。さらに実行コンテキストや JSONata 変数の注入も AWS CLI から検証しています。

--mock--state-name を組み合わせることで、特定の実行経路におけるデータ受け渡しと遷移先を段階的に検証できます。

なお、モックを使った Map/Parallel の検証では内部の ItemProcessor や Branch 内のステートを実行するのではなく、モック結果をステート全体の結果として扱います。そのため、内部処理そのものよりも、入出力変換や期待する出力形状の確認に向いています。

ステート単位の検証や特定経路の確認を自動化したい場面で、有力な選択肢になりそうです。

参考リンク

https://docs.aws.amazon.com/step-functions/latest/apireference/API_TestState.html

https://github.com/aws-samples/sample-stepfunctions-testing-with-testStateAPI/

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事