Skip to content

Commit

Permalink
SAM Test for StepFunctions (aws#378)
Browse files Browse the repository at this point in the history
* SAM test for step functions (state machine)

* Fixing a typo in comment

* Added missing unit test for create test executors of kinesis and stepfunctions

* Refactoring to change the lambda json converter the default json converter
  • Loading branch information
qingchm authored Aug 3, 2021
1 parent 1b7cd64 commit 62a8159
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 11 deletions.
4 changes: 2 additions & 2 deletions samcli/lib/test/lambda_test_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def _execute_action(self, payload: str):
return self._lambda_client.invoke(FunctionName=self._function_name, Payload=payload)


class LambdaConvertToDefaultJSON(TestRequestResponseMapper):
class DefaultConvertToJSON(TestRequestResponseMapper):
"""
If a regular string is provided as payload, this class will convert it into a JSON object
"""
Expand All @@ -41,7 +41,7 @@ def map(self, test_input: TestExecutionInfo) -> TestExecutionInfo:
try:
_ = json.loads(cast(str, test_input.payload))
except JSONDecodeError:
json_value = f'"{test_input.payload}"'
json_value = json.dumps(test_input.payload)
LOG.info(
"Auto converting value '%s' into JSON '%s'. "
"If you don't want auto-conversion, please provide a JSON string as payload",
Expand Down
38 changes: 38 additions & 0 deletions samcli/lib/test/stepfunctions_test_executor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""
Test executor implementation for StepFunctions
"""
import logging
import time
from typing import Any

from samcli.lib.test.test_executors import BotoActionExecutor

LOG = logging.getLogger(__name__)


class StepFunctionsStartExecutionExecutor(BotoActionExecutor):
"""
Calls "start_execution" method of "stepfunctions" service with given input.
If a file location provided, the file handle will be passed as Payload object
"""

_stepfunctions_client: Any
_physical_id: str
_state_machine_arn: str

def __init__(self, stepfunctions_client: Any, physical_id: str):
self._stepfunctions_client = stepfunctions_client
self._state_machine_arn = physical_id

def _execute_action(self, payload: str):
timestamp = str(int(time.time() * 1000))
name = f"sam_test_{timestamp}"
LOG.debug(
"Calling stepfunctions_client.start_execution with name:%s, input:%s, stateMachineArn:%s",
name,
payload,
self._state_machine_arn,
)
return self._stepfunctions_client.start_execution(
name=name, input=payload, stateMachineArn=self._state_machine_arn
)
25 changes: 22 additions & 3 deletions samcli/lib/test/test_executor_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,20 @@
import logging
from typing import Dict, Callable, Any, Optional

from samcli.commands._utils.resources import AWS_LAMBDA_FUNCTION, AWS_SQS_QUEUE, AWS_KINESIS_STREAM
from samcli.commands._utils.resources import (
AWS_LAMBDA_FUNCTION,
AWS_SQS_QUEUE,
AWS_KINESIS_STREAM,
AWS_STEPFUNCTIONS_STATEMACHINE,
)
from samcli.lib.test.lambda_test_executor import (
LambdaConvertToDefaultJSON,
DefaultConvertToJSON,
LambdaResponseConverter,
LambdaInvokeExecutor,
)
from samcli.lib.test.sqs_test_executor import SqsSendMessageExecutor, SqsConvertToEntriesJsonObject
from samcli.lib.test.kinesis_test_executor import KinesisPutRecordsExecutor, KinesisConvertToRecordsJsonObject
from samcli.lib.test.stepfunctions_test_executor import StepFunctionsStartExecutionExecutor
from samcli.lib.test.test_executors import TestExecutor, ResponseObjectToJsonStringMapper
from samcli.lib.utils.cloudformation import CloudFormationResourceSummary

Expand Down Expand Up @@ -57,7 +63,7 @@ def create_test_executor(self, cfn_resource_summary: CloudFormationResourceSumma

def _create_lambda_test_executor(self, cfn_resource_summary: CloudFormationResourceSummary):
return TestExecutor(
request_mappers=[LambdaConvertToDefaultJSON()],
request_mappers=[DefaultConvertToJSON()],
response_mappers=[LambdaResponseConverter(), ResponseObjectToJsonStringMapper()],
boto_action_executor=LambdaInvokeExecutor(
self._boto_client_provider("lambda"),
Expand Down Expand Up @@ -89,9 +95,22 @@ def _create_kinesis_test_executor(self, cfn_resource_summary: CloudFormationReso
),
)

def _create_stepfunctions_test_executor(self, cfn_resource_summary: CloudFormationResourceSummary):
return TestExecutor(
request_mappers=[
DefaultConvertToJSON(),
],
response_mappers=[ResponseObjectToJsonStringMapper()],
boto_action_executor=StepFunctionsStartExecutionExecutor(
self._boto_client_provider("stepfunctions"),
cfn_resource_summary.physical_resource_id,
),
)

# mapping definition for each supported resource type
EXECUTOR_MAPPING: Dict[str, Callable[["TestExecutorFactory", CloudFormationResourceSummary], TestExecutor]] = {
AWS_LAMBDA_FUNCTION: _create_lambda_test_executor,
AWS_SQS_QUEUE: _create_sqs_test_executor,
AWS_KINESIS_STREAM: _create_kinesis_test_executor,
AWS_STEPFUNCTIONS_STATEMACHINE: _create_stepfunctions_test_executor,
}
6 changes: 3 additions & 3 deletions tests/unit/lib/test/test_lambda_test_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from samcli.lib.test.lambda_test_executor import (
LambdaInvokeExecutor,
LambdaConvertToDefaultJSON,
DefaultConvertToJSON,
LambdaResponseConverter,
)
from samcli.lib.test.test_executors import TestExecutionInfo
Expand All @@ -26,9 +26,9 @@ def test_execute_action(self):
self.lambda_client.invoke.assert_called_with(FunctionName=self.function_name, Payload=given_payload)


class TestLambdaConvertToDefaultJSON(TestCase):
class TestDefaultConvertToJSON(TestCase):
def setUp(self) -> None:
self.lambda_convert_to_default_json = LambdaConvertToDefaultJSON()
self.lambda_convert_to_default_json = DefaultConvertToJSON()

def test_conversion(self):
given_string = "Hello World"
Expand Down
29 changes: 29 additions & 0 deletions tests/unit/lib/test/test_stepfunctions_test_executor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import base64

from json import JSONDecodeError
from typing import Any
from unittest import TestCase
from unittest.mock import ANY, Mock, patch

from samcli.lib.test.stepfunctions_test_executor import StepFunctionsStartExecutionExecutor


class TestStepFunctionsStartExecutionExecutor(TestCase):
def setUp(self) -> None:
self.stepfunctions_client = Mock()
self.statemachine_arn = Mock()
self.stepfunctions_start_execution_executor = StepFunctionsStartExecutionExecutor(
self.stepfunctions_client, self.statemachine_arn
)

def test_execute_action(self):
given_payload = Mock()
given_result = Mock()
self.stepfunctions_client.start_execution.return_value = given_result

result = self.stepfunctions_start_execution_executor._execute_action(given_payload)

self.assertEqual(result, given_result)
self.stepfunctions_client.start_execution.assert_called_with(
name=ANY, input=given_payload, stateMachineArn=self.statemachine_arn
)
86 changes: 83 additions & 3 deletions tests/unit/lib/test/test_test_executor_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def test_failed_create_test_executor(self):
self.assertIsNone(executor)

@patch("samcli.lib.test.test_executor_factory.LambdaInvokeExecutor")
@patch("samcli.lib.test.test_executor_factory.LambdaConvertToDefaultJSON")
@patch("samcli.lib.test.test_executor_factory.DefaultConvertToJSON")
@patch("samcli.lib.test.test_executor_factory.LambdaResponseConverter")
@patch("samcli.lib.test.test_executor_factory.ResponseObjectToJsonStringMapper")
@patch("samcli.lib.test.test_executor_factory.TestExecutor")
Expand Down Expand Up @@ -87,9 +87,9 @@ def test_create_sqs_test_executor(
given_test_executor = Mock()
patched_test_executor.return_value = given_test_executor

lambda_executor = self.test_executor_factory._create_sqs_test_executor(given_cfn_resource_summary)
sqs_executor = self.test_executor_factory._create_sqs_test_executor(given_cfn_resource_summary)

self.assertEqual(lambda_executor, given_test_executor)
self.assertEqual(sqs_executor, given_test_executor)

patched_convert_to_json.assert_called_once()
patched_convert_response_to_string.assert_called_once()
Expand All @@ -104,3 +104,83 @@ def test_create_sqs_test_executor(
response_mappers=[patched_convert_response_to_string()],
boto_action_executor=patched_sqs_message_executor(),
)

@patch("samcli.lib.test.test_executor_factory.KinesisPutRecordsExecutor")
@patch("samcli.lib.test.test_executor_factory.ResponseObjectToJsonStringMapper")
@patch("samcli.lib.test.test_executor_factory.KinesisConvertToRecordsJsonObject")
@patch("samcli.lib.test.test_executor_factory.TestExecutor")
def test_create_kinesis_test_executor(
self,
patched_test_executor,
patched_convert_to_json,
patched_convert_response_to_string,
patched_kinesis_put_records_executor,
):
given_physical_resource_id = "physical_resource_id"
given_cfn_resource_summary = Mock(physical_resource_id="physical_resource_id")

given_kinesis_client = Mock()
self.boto_client_provider_mock.return_value = given_kinesis_client

given_test_executor = Mock()
patched_test_executor.return_value = given_test_executor

kinesis_executor = self.test_executor_factory._create_kinesis_test_executor(given_cfn_resource_summary)

self.assertEqual(kinesis_executor, given_test_executor)

patched_convert_to_json.assert_called_once()
patched_convert_response_to_string.assert_called_once()

self.boto_client_provider_mock.assert_called_with("kinesis")
patched_kinesis_put_records_executor.assert_called_with(given_kinesis_client, given_physical_resource_id)

patched_test_executor.assert_called_with(
request_mappers=[
patched_convert_to_json(),
],
response_mappers=[patched_convert_response_to_string()],
boto_action_executor=patched_kinesis_put_records_executor(),
)

@patch("samcli.lib.test.test_executor_factory.StepFunctionsStartExecutionExecutor")
@patch("samcli.lib.test.test_executor_factory.ResponseObjectToJsonStringMapper")
@patch("samcli.lib.test.test_executor_factory.DefaultConvertToJSON")
@patch("samcli.lib.test.test_executor_factory.TestExecutor")
def test_create_stepfunctions_test_executor(
self,
patched_test_executor,
patched_convert_to_json,
patched_convert_response_to_string,
patched_stepfunctions_start_execution_executor,
):
given_physical_resource_id = "physical_resource_id"
given_cfn_resource_summary = Mock(physical_resource_id="physical_resource_id")

given_stepfunctions_client = Mock()
self.boto_client_provider_mock.return_value = given_stepfunctions_client

given_test_executor = Mock()
patched_test_executor.return_value = given_test_executor

stepfunctions_executor = self.test_executor_factory._create_stepfunctions_test_executor(
given_cfn_resource_summary
)

self.assertEqual(stepfunctions_executor, given_test_executor)

patched_convert_to_json.assert_called_once()
patched_convert_response_to_string.assert_called_once()

self.boto_client_provider_mock.assert_called_with("stepfunctions")
patched_stepfunctions_start_execution_executor.assert_called_with(
given_stepfunctions_client, given_physical_resource_id
)

patched_test_executor.assert_called_with(
request_mappers=[
patched_convert_to_json(),
],
response_mappers=[patched_convert_response_to_string()],
boto_action_executor=patched_stepfunctions_start_execution_executor(),
)

0 comments on commit 62a8159

Please sign in to comment.