Skill 116 · AWS Step Functions
Subchapter 116.8
references/validation-and-testing.mdMarkdown11 KBView on GitHub
Files saved with the .asl.json extension get automatic validation from the AWS Toolkit Extension. If the extension is not installed, suggest the user install it (https://open-vsx.org/extension/amazonwebservices/aws-toolkit-vscode). Use your diagnostics tool on any file to catch structural errors instantly. The State Machine definition must be saved as to work with local validation.
.asl.json.asl.jsonThe TestState API enables unit and integration testing of Step Functions without deployment. Key capabilities:
.sync, .waitForTaskToken (require mocks)roleArn optionalBefore calling the TestState API, follow this sequence:
aws sts get-caller-identity and verify the response.Prefer short-lived credentials: Use credentials from an IAM role (EC2 instance profile, ECS task role, or AWS IAM Identity Center) rather than long-lived access keys stored in
~/.aws/credentials.
The calling identity needs states:TestState. If not using mocks, it also needs iam:PassRole for the execution role. For HTTP Task with revealSecrets, add states:RevealSecrets.
aws stepfunctions test-state \
--definition '{"Type":"Task","Resource":"arn:aws:states:::lambda:invoke","Arguments":{...},"End":true}' \
--input '{"data":"value"}' \
--mock '{"result":"{\"StatusCode\":200,\"Payload\":{\"body\":\"success\"}}"}' \
--inspection-level DEBUG| Level | Returns | Use Case |
|---|---|---|
| INFO | output, status, nextState | Quick validation |
| DEBUG | + afterArguments, result, variables | Data flow debugging |
| TRACE | + HTTP request/response (use --reveal-secrets for auth) | HTTP Task debugging |
⚠️ Mocks MUST match AWS service API response schema exactly — field names (case-sensitive), types, required fields.
Resource ARN: arn:aws:states:::lambda:invoke → Lambda Invoke API| Service | API | Mock Structure | Example |
|---|---|---|---|
| Lambda | Invoke | {StatusCode, Payload, FunctionError?} | '{"result":"{\"StatusCode\":200,\"Payload\":{\"body\":\"ok\"}}"}' |
| DynamoDB | PutItem | {Attributes?} | '{"result":"{\"Attributes\":{\"id\":{\"S\":\"123\"}}}"}' |
| DynamoDB | GetItem | {Item?} | '{"result":"{\"Item\":{\"id\":{\"S\":\"123\"}}}"}' |
| SNS | Publish | {MessageId} | '{"result":"{\"MessageId\":\"abc-123\"}"}' |
| SQS | SendMessage | {MessageId, MD5OfMessageBody} | '{"result":"{\"MessageId\":\"xyz\",\"MD5OfMessageBody\":\"...\"}"}' |
| EventBridge | PutEvents | {FailedEntryCount, Entries[]} | '{"result":"{\"FailedEntryCount\":0,\"Entries\":[{\"EventId\":\"123\"}]}"}' |
| S3 | PutObject | {ETag, VersionId?} | '{"result":"{\"ETag\":\"\\\"abc123\\\"\"}"}' |
| Step Functions | StartExecution | {ExecutionArn, StartDate} | '{"result":"{\"ExecutionArn\":\"arn:...\",\"StartDate\":\"...\"}"}' |
| Secrets Manager | GetSecretValue | {ARN, Name, SecretString?} | '{"result":"{\"Name\":\"MySecret\",\"SecretString\":\"...\"}"}' |
For .sync patterns: Mock the polling API (e.g., startExecution.sync:2 → mock DescribeExecution, NOT StartExecution)
Success: --mock '{"result":"<service API response JSON>"}'
Error: --mock '{"errorOutput":{"error":"ErrorCode","cause":"description"}}'
Validation: --mock '{"fieldValidationMode":"STRICT|PRESENT|NONE","result":"..."}'
Validation modes:
STRICT (default): All required fields, correct types — use in CI/CDPRESENT: Only validate fields present — flexible testingNONE: No validation — quick prototyping onlyTests Map’s input/output processing, not iterations inside. Mock = entire Map output.
aws stepfunctions test-state \
--definition '{
"Type":"Map",
"Items":"{% $states.input.items %}",
"ItemSelector":{"value":"{% $states.context.Map.Item.Value %}"},
"ItemProcessor":{"ProcessorConfig":{"Mode":"INLINE"},...},
"End":true
}' \
--input '{"items":[1,2,3]}' \
--mock '{"result":"[10,20,30]"}' \
--inspection-level DEBUGDEBUG returns: afterItemSelector, afterItemBatcher, toleratedFailureCount, maxConcurrency
Distributed Map: Provide data in input (as if read from S3)
Failure threshold testing: Use --state-configuration '{"mapIterationFailureCount":N}'
Testing state within Map: --state-name auto-populates $states.context.Map.Item.Index, $states.context.Map.Item.Value
Mock = JSON array, one element per branch (in definition order):
--mock '{"result":"[{\"branch1\":\"result1\"},{\"branch2\":\"result2\"}]"}'--state-configuration '{"retrierRetryCount":1}' \
--mock '{"errorOutput":{"error":"Lambda.ServiceException","cause":"..."}}' \
--inspection-level DEBUGResponse includes: status:"RETRIABLE", retryBackoffIntervalSeconds, retryIndex
--mock '{"errorOutput":{"error":"Lambda.TooManyRequestsException","cause":"..."}}' \
--inspection-level DEBUGResponse includes: status:"CAUGHT_ERROR", nextState, catchIndex, error in output
--state-name "ChildState" \
--state-configuration '{"errorCausedByState":"ChildState"}' \
--mock '{"errorOutput":{"error":"States.TaskFailed","cause":"..."}}'Required: Must provide mock (validation exception otherwise)
Mock the polling API, not initial call:
# startExecution.sync:2 → mock DescribeExecution
--mock '{"result":"{\"Status\":\"SUCCEEDED\",\"Output\":\"{...}\"}"}'Common patterns: startExecution.sync:2→DescribeExecution, batch:submitJob.sync→DescribeJobs, glue:startJobRun.sync→GetJobRun
--context '{"Task":{"Token":"test-token-123"}}' \
--mock '{"result":"{\"StatusCode\":200,\"Payload\":{\"status\":\"approved\"}}"}'Require mock:
--definition '{"Type":"Task","Resource":"arn:aws:states:...:activity:MyActivity",...}' \
--mock '{"result":"{\"result\":\"completed\"}"}'RESULT_1=$(aws stepfunctions test-state --state-name "State1" ... | jq -r '.output')
NEXT_1=$(... | jq -r '.nextState')
RESULT_2=$(aws stepfunctions test-state --state-name "$NEXT_1" --input "$RESULT_1" ...)Validates: data transformations, state transitions, end-to-end paths
Test states referencing execution context:
--context '{
"Execution":{"Id":"arn:...","Name":"test-123","StartTime":"2024-01-01T10:00:00.000Z"},
"State":{"Name":"ProcessData","EnteredTime":"2024-01-01T10:00:05.000Z"},
"Task":{"Token":"test-token-abc123"}
}'--resource "arn:aws:states:::http:invoke" \
--inspection-level TRACE \
--reveal-secrets # Requires states:RevealSecrets permissionReturns: inspectionData.request (method, URL, headers, body), inspectionData.response (status, headers, body)
| Error | Fix |
|---|---|
| Invalid field type | Check AWS SDK docs for correct types |
| Required field missing | Add field OR use fieldValidationMode:PRESENT |
| .sync validation failed | Mock polling API, not initial call |
Debug workflow:
fieldValidationMode:NONE for logic testingPRESENT for partial validationSTRICT in CI/CD#!/bin/bash
test_state() {
local state_name=$1
local input=$2
local mock=$3
aws stepfunctions test-state \
--definition "$(cat statemachine.asl.json)" \
--state-name "$state_name" \
--input "$input" \
--mock "$mock" \
--inspection-level DEBUG
}
# Test chain
RESULT=$(test_state "State1" '{"id":"123"}' '{"result":"..."}' | jq -r '.output')
test_state "State2" "$RESULT" '{"result":"..."}'mapIterationFailureCount--reveal-secrets output — keep it out of version control, shell history, CI/CD build logs, and CloudWatch Logs; redirect to a file with restricted permissions and avoid persisting DEBUG/TRACE output that may contain secrets# Basic test
aws stepfunctions test-state --definition '{...}' --input '{...}' --mock '{...}'
# Test specific state in state machine
aws stepfunctions test-state --definition "$(cat sm.json)" --state-name "MyState" --input '{...}' --mock '{...}'
# Test retry (2nd attempt)
--state-configuration '{"retrierRetryCount":1}' --mock '{"errorOutput":{...}}'
# Test Map failure threshold
--state-configuration '{"mapIterationFailureCount":5}' --mock '{"errorOutput":{...}}'
# Test with context
--context '{"Execution":{"Id":"..."}, "Task":{"Token":"..."}}'
# HTTP Task with secrets
--inspection-level TRACE --reveal-secrets
# Mock validation modes
--mock '{"fieldValidationMode":"STRICT|PRESENT|NONE","result":"..."}'