Package: pytest-stepfunctions
pytest-stepfunctionsA pytest fixture that makes you able to mock Lambda code during AWS StepFunctions local testing.Table of ContentsOverviewInstallingGetting StartedCreating a State MachineMocking the EMR Client in the Lambda CodeStarting Execution and Validating ResultsRunning the Test with the Step Functions JARRunning the Test with the Step Functions Docker ImageContributingKnown IssuesOverviewAWS provides local Step Functions as a JAR and a Docker image for the quick testing without deployment. They described how to perform such task inthis articleas well. I got excited at the very beginning, but soon ended up frustrated for still being unable to mock Lambda functions' external dependencies. Then I thought: what if initiate a Python thread with a fake Lambda service and use this fake service to execute Lambda functions? Fortunately, It works!InstallingUse pip to install:$pipinstallpytest-stepfunctionsGetting StartedSuppose there is a state machine that simply collects all the EMR cluster unique identifiers. Here is the state machine definition:{"StartAt":"ListIds","States":{"ListIds":{"Type":"Task","Resource":"${ListIdsLambdaArn}","ResultPath":"$.cluster_ids","End":true}}}and the Lambda codemy/pkg/emr.py:importboto3deflist_ids(*args,**kwargs):emr_client=boto3.client("emr")response=emr_client.list_clusters()return[item["Id"]foriteminresponse["Clusters"]]Creating a State MachineIn the test filetests/test_foo.py, create a Step Functions client with endpoint URL pointed to our Step Functions service, and use this client to create a state machine resource by using the definition abovefromstringimportTemplateimportboto3deftest_bar(aws_stepfunctions_endpoint_url):definition_template=Template("""{"StartAt": "ListIds","States": {"ListIds": {"Type": "Task","Resource": "${ListIdsLambdaArn}","ResultPath": "$.cluster_ids","End": true}}}""")list_ids_lambda_arn="arn:aws:lambda:us-east-1:123456789012:function:my.pkg.emr.list_ids"definition=definition_template.safe_substitute(ListIdsLambdaArn=list_ids_lambda_arn)sfn_client=boto3.client("stepfunctions",endpoint_url=aws_stepfunctions_endpoint_url)state_machine_arn=sfn_client.create_state_machine(name="list-ids",definition=definition,roleArn="arn:aws:iam::012345678901:role/DummyRole")["stateMachineArn"]Note the internal fake Lambda service in pytest-stepfunctions will parse Lambda ARNs to recognize what to call.Mocking the EMR Client in the Lambda CodeHere uses thepytest-mockfixture to temporarily patch theboto3module inside the Lambda code.botocore.stub.Stubberis also applied to make sure the mock request parameters and response content are all valid:frombotocore.stubimportStubberdeftest_bar(aws_stepfunctions_endpoint_url,mocker):...emr_client=boto3.client("emr")mocker.patch("my.pkg.emr.boto3",autospec=True).client.return_value=emr_clientstubber=Stubber(emr_client)stubber.add_response("list_clusters",service_response={"Clusters":[{"Id":"j-00001"},{"Id":"j-00002"}]})Starting Execution and Validating ResultsStart and wait until the execution status is notRUNNING:importjsonimporttimedeftest_bar(aws_stepfunctions_endpoint_url,mocker):...execution_arn=sfn_client.start_execution(stateMachineArn=state_machine_arn,name="list-ids-exec",input="{}")["executionArn"]withstubber:whileTrue:response=sfn_client.describe_execution(executionArn=execution_arn)ifresponse["status"]!="RUNNING":breaktime.sleep(0.5)stubber.assert_no_pending_responses()assert"SUCCEEDED"==response["status"]assert["j-00001","j-00002"]==json.loads(response["output"])["cluster_ids"]Running the Test with the Step Functions JARThe JAR is availablehere. Download and execute it first:$java-jar/path/to/StepFunctionsLocal.jar\--lambda-endpointhttp://localhost:13000\--step-functions-endpointhttp://localhost:8083\--wait-time-scale0StepFunctionsLocal
Version:1.4.0
Build:2019-09-182020-07-0618:40:28.284:Configure[LambdaEndpoint]to[http://localhost:13000]2020-07-0618:40:28.285:Configure[StepFunctionsEndpoint]to[http://localhost:8083]2020-07-0618:40:28.323:Loadedcredentialsfromprofile:default2020-07-0618:40:28.324:Startingserveronport8083withaccount123456789012,regionus-east-1Then run the test with the following command:$python-mpytest-v\--pytest-stepfunctions-endpoint-url=http://0.0.0.0:8083\--pytest-stepfunctions-lambda-address=0.0.0.0\--pytest-stepfunctions-lambda-port=13000\./tests===============================testsessionstarts================================platformlinux--Python3.7.3,pytest-5.4.3,py-1.9.0,pluggy-0.13.1--/tmp/gg/venv/bin/python
cachedir:.pytest_cache
rootdir:/tmp/gg
plugins:mock-3.1.1,stepfunctions-0.1a2
collected1item

tests/test_foo.py::test_barPASSED[100%]================================1passedin1.01s=================================Running the Test with the Step Functions Docker ImageI personally recommend this way as it is much easier to reproduce the testing environment.This is theDockerfileFROMpython:3.7WORKDIR/appCOPY./my./myCOPY./tests./testsRUNpipinstallpytestpytest-stepfunctionspytest-mockboto3and thedocker-compose.ymlfor Docker Compose:version:"3.2"services:tester:build:context:.dockerfile:./Dockerfileenvironment:AWS_DEFAULT_REGION:us-east-1AWS_ACCESS_KEY_ID:xxxAWS_SECRET_ACCESS_KEY:xxxcommand:>bash -c "python -m pytest -v--pytest-stepfunctions-endpoint-url=http://sfn-endpoint:8083--pytest-stepfunctions-lambda-address=0.0.0.0--pytest-stepfunctions-lambda-port=13000./tests"sfn-endpoint:image:amazon/aws-stepfunctions-local:1.5.1environment:AWS_DEFAULT_REGION:us-east-1AWS_ACCESS_KEY_ID:xxxAWS_SECRET_ACCESS_KEY:xxxWAIT_TIME_SCALE:0STEP_FUNCTIONS_ENDPOINT:http://sfn-endpoint:8083LAMBDA_ENDPOINT:http://tester:13000Then run the following command to run the test:$docker-composeup--build--exit-code-fromtesterContributingHere are the tools required:Docker and Docker ComposeGNU Makepip-toolsSet up a virtual environment for your IDE:$python-mvenvvenv
$source./venv/bin/activate(venv)$pip-sync./dev-requirements.txt./requirements.txtRun the tests and linters:$makelinttestKnown IssuesNested workflows are very slow: if a state machine contains lots of nested state machines, the execution will be extremely slow evenWAIT_TIME_SCALEis set to 0. This is a known performance issue in the official JAR.AWS Service integrations other than Lambda are not supported yet. Services like EMR even have no endpoint option in the official JAR. A possible workaround for some cases is calling them by invoking Lambda functions.