How to write and execute integration tests for AWS CDK applications
July 21, 2023Automated integration testing validates system components and boosts confidence for new software releases. Performing integration tests on resources deployed to the AWS cloud enables the validation of AWS Identity and Access Management (IAM) policies, service limits, application configuration, and runtime code. For developers that are currently leveraging AWS Cloud Development Kit (AWS CDK) as their Infrastructure as Code tool, there is a testing framework available that makes integration testing easier to implement in the software release.
AWS CDK is an open-source framework for defining and provisioning AWS cloud infrastructure using supported programming languages. The framework includes constructs for writing and running unit and integration tests. The assertions construct can be used to write unit tests and assert against the generated CloudFormation templates. CDK integ-tests construct can be used for defining integration test cases and can be combined with CDK integ-runner for executing these tests. The integ-runner handles automatic resource provisioning and removal and supports several customization options. Unit tests using assertion functions are used to test configurations in the CloudFormation templates before deploying these templates, while integration tests run assertions in the deployed resources. This blog post demonstrates writing automated integration tests for an example application using AWS CDK.
Solution Overview
The example application shown in Figure 1 is a sample serverless data enrichment application. Data is processed and enriched in the system as follows:
- Users publish messages to an Amazon Simple Notification Service (Amazon SNS) topic. Messages are encrypted at rest using an AWS Key Management Service (AWS KMS) customer-managed key.
- Amazon Simple Queue Service (Amazon SQS) queue is subscribed to the Amazon SNS topic, where published messages are delivered.
- AWS Lambda consumes messages from the Amazon SQS queue, adding additional data to the message. Messages that cannot be processed successfully are sent to a dead-letter queue.
- Successfully enriched messages are stored in an Amazon DynamoDB table by the Lambda function.
For this sample application, we will use AWS CDK’s integration testing framework to validate the processing for a single message as shown in Figure 2. To run the test, we configure the test framework to do the following steps:
- Publish a message to the Amazon SNS topic. Wait for the application to process the message and save to DynamoDB.
- Periodically check the Amazon DynamoDB table and verify that the saved message was enriched.
Prerequisites
The following are the required to deploy this solution:
- An AWS account
- Node.js v16 or later and npm version 9 or later are installed
- Install AWS CDK version 2.73.0 or later
- Clone the GitHub repository and install the dependencies
- Run CDK Bootstrap on your AWS Account, in N. Virginia (us-east-1) region
The structure of the sample AWS CDK application repository is as follows:
- /bin folder contains the top-level definition of the AWS CDK app.
- /lib folder contains the stack definition of the application under test which defines the application described in the section above.
- /lib/functions contains the Lambda function runtime code.
- /integ-tests contains the integration test stack where we define and configure our test cases.
The repository is a typical AWS CDK application except that it has one additional directory for the test case definitions. For the remainder of this blog post, we focus on the integration test definition in /integ-tests/integ.sns-sqs-ddb.ts and walk you through its creation and the execution of the integration test.
Writing integration tests
An integration test should validate expected behavior of your AWS CDK application. You can define an integration test for your application as follows:
- Create a stack under test from the CdkIntegTestsDemoStack definition and map it to the application.
// CDK App for Integration Tests const app = new cdk.App(); // Stack under test const stackUnderTest = new CdkIntegTestsDemoStack(app, ‘IntegrationTestStack’, { setDestroyPolicyToAllResources: true, description: “This stack includes the application’s resources for integration testing.”, });
- Define the integration test construct with a list of test cases. This construct offers the ability to customize the behavior of the integration runner tool. For example, you can force the integ-runner to destroy the resources after the test run to force the cleanup.
// Initialize Integ Test construct const integ = new IntegTest(app, ‘DataFlowTest’, { testCases: [stackUnderTest], // Define a list of cases for this test cdkCommandOptions: { // Customize the integ-runner parameters destroy: { args: { force: true, }, }, }, regions: [stackUnderTest.region], });
- Add an assertion to validate the test results. In this example, we validate the single message flow from the Amazon SNS topic to the Amazon DynamoDB table. The assertion publishes the message object to the Amazon SNS topic using the AwsApiCall method. In the background this method utilizes a Lambda-backed CloudFormation custom resource to execute the Amazon SNS Publish API call with the AWS SDK for JavaScript.
/** * Assertion: * The application should handle single message and write the enriched item to the DynamoDB table. */ const id = 'test-id-1'; const message = 'This message should be validated'; /** * Publish a message to the SNS topic. * Note - SNS topic ARN is a member variable of the * application stack for testing purposes. */ const assertion = integ.assertions .awsApiCall('SNS', 'publish', { TopicArn: stackUnderTest.topicArn, Message: JSON.stringify({ id: id, message: message, }), })
- Use the next helper method to chain API calls. In our example, a second Amazon DynamoDB GetItem API call gets the item whose primary key equals the message id. The result from the second API call is expected to match the message object including the additional attribute added as a result of the data enrichment.
/** * Validate that the DynamoDB table contains the enriched message. */ .next( integ.assertions .awsApiCall('DynamoDB', 'getItem', { TableName: stackUnderTest.tableName, Key: { id: { S: id } }, }) /** * Expect the enriched message to be returned. */ .expect( ExpectedResult.objectLike({ Item: { id: { S: id, }, message: { S: message, }, additionalAttr: { S: 'enriched', }, }, }), )
- Since it may take a while for the message to be passed through the application, we run the assertion asynchronously by calling the waitForAssertions method. This means that the Amazon DynamoDB GetItem API call is called in intervals until the expected result is met or the total timeout is reached.
/** * Timeout and interval check for assertion to be true. * Note - Data may take some time to arrive in DynamoDB. * Iteratively executes API call at specified interval. */ .waitForAssertions({ totalTimeout: Duration.seconds(25), interval: Duration.seconds(3), }), );
- The AwsApiCall method automatically adds the correct IAM permissions for both API calls to the AWS Lambda function. Given that the example application’s Amazon SNS topic is encrypted using an AWS KMS key, additional permissions are required to publish the message.
// Add the required permissions to the api call assertion.provider.addToRolePolicy({ Effect: 'Allow', Action: [ 'kms:Encrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*', 'kms:Decrypt', ], Resource: [stackUnderTest.kmsKeyArn], });
The full code for this blog is available on this GitHub project.
Running integration tests
In this section, we show how to run integration test for the introduced sample application using the integ-runner to execute the test case and report on the assertion results.
Install and build the project.
npm install npm run build
Run the following command to initiate the test case execution with a list of options.
npm run integ-test
The directory option specifies in which location the integ-runner needs to recursively search for test definition files. The parallel-regions option allows to define a list of regions to run tests in. We set this to us-east-1 and ensure that the AWS CDK bootstrapping has previously been performed in this region. The update-on-failed option allows to rerun the integration tests if the snapshot fails. A full list of available options can be found in the integ-runner Github repository.
Hint: if you want to retain your test stacks during development for debugging, you can specify the no-clean option to retain the test stack after the test run.
The integ-runner initially checks the integration test snapshots to determine if any changes have occurred since the last execution. Since there are no previous snapshots for the initial run, the snapshot verification fails. As a result, the integ-runner begins executing the integration tests using the ephemeral test stack and displays the result.
Verifying integration test snapshots... NEW integ.sns-sqs-ddb 2.863s Snapshot Results: Tests: 1 failed, 1 total Running integration tests for failed tests... Running in parallel across regions: us-east-1
Running test <your-path>/cdk-integ-tests-demo/integ-tests/integ.sns-sqs-ddb.js in us-east-1 SUCCESS integ.sns-sqs-ddb-DemoTest/DefaultTest 587.295s AssertionResultsAwsApiCallDynamoDBgetItem - success Test Results: Tests: 1 passed, 1 total
The integ-runner generates two AWS CloudFormation stacks, as shown in Figure 3. The IntegrationTestStack stack includes the resources from our sample application, which serves as an isolated application representing the stack under test. The DataFlowDefaultTestDeployAssert stack contains the resources required for executing the integration tests as shown in Figure 4.
Cleaning up
Based on the specified RemovalPolicy, the resources are automatically destroyed as the stack is removed. Some resources such as Amazon DynamoDB tables have the default RemovalPolicy set to Retain in AWS CDK. To set the removal policy to Destroy for the integration test resources, we leverage Aspects.
/** * Aspect for setting all removal policies to DESTROY */
class ApplyDestroyPolicyAspect implements cdk.IAspect { public visit(node: IConstruct): void { if (node instanceof CfnResource) { node.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY); } }
}
If you set the no-clean argument as part of the integ-runner CLI options, you need to manually destroy the stacks. This can be done from the AWS Console, via AWS CloudFormation as shown in Figure 5 or by using the following command.
cdk destroy --all
To clean up the code repository build files, you can run the following script.
npm run clean
Conclusion
The AWS CDK integ-tests construct is a valuable tool for defining and conducting automated integration tests for your AWS CDK applications. In this blog post, we have introduced a practical code example showcasing how AWS CDK integration tests can be used to validate the expected application behavior when deployed to the cloud. You can leverage the techniques in this guide to write your own AWS CDK integration tests and improve the quality and reliability of your application releases.
For information on how to get started with these constructs, please refer to the following documentation.
Call to Action
Integ-runner and integ-tests constructs are experimental and subject to change. The release notes for both stable and experimental modules are available in the AWS CDK Github release notes. As always, we welcome bug reports, feature requests, and pull requests on the aws-cdk GitHub repository to further shape these alpha constructs based on your feedback.