How to write and execute integration tests for AWS CDK applications
Automated integration testing validates system components and increases confidence in new software releases. Running integration tests on resources deployed in the AWS cloud enables verification of AWS Identity and Access Management (IAM) policies, service limits, application configuration, and execution code. For developers currently using the AWS Cloud Development Kit (AWS CDK) as an infrastructure and code tool, a testing platform is available to facilitate the deployment of integration testing in a given software release.
AWS CDK is an open-source platform for defining and releasing AWS cloud infrastructure using supported programming languages. The framework includes constructs for writing and running entities and integration tests. Assertion constructs can be used to write unit tests and confirm against generated CloudFormation templates. The CDK integ-tests construct can be detected to define integration test cases and combined with the CDK integ-runner to execute these tests. Integ-runner supports the automatic sharing and deletion of resources and promotes several customization options. Unit tests using assertion functions are used to test configurations in CloudFormation templates before deploying these templates, while integration tests run assertions on deployed resources. The following article demonstrates how automated integration tests for a sample application can be written using AWS CDK.
The example application shown in Figure 1 is an example serverless data enrichment application. Data is processed and enriched in the system as follows:
- Users post messages in the Amazon Simple Notification Service (Amazon SNS) subject line. Messages are encrypted at rest using a key managed by the AWS Key Management Service (AWS KMS) client.
- The Amazon Simple Queue Service (Amazon SQS) queue is subscribed to the Amazon SNS topic to which published messages are delivered.
- AWS Lambda absorbs messages from the Amazon SQS queue, adding additional data to the message. Messages that cannot be successfully processed are sent to the lost message queue.
- Successfully enriched messages are stored in an Amazon DynamoDB table by the Lambda function.
In this sample application, developers will use the AWS CDK integration testing platform to validate the processing of a single message, as shown in Figure 2. To run the test, they will configure the test platform to perform the following steps:
- Publish the message in the Amazon SNS subject line. Wait for the application to process the message and save it in DynamoDB.
- Periodically check the Amazon DynamoDB table and verify that the saved message has been enriched.
Prerequisites
The following are the requirements for implementing the solution in question:
- AWS account
- Node.js version 16 or later and npm version 9 or later
- Install AWS CDK version 2.73.0 or later
- Clone the GitHub repository and install the dependencies
- Run Bootstrap CDK on your AWS account in the N. Virginia (us-east-1).
The structure of the sample AWS CDK application repository is as follows:
- The /bin folder contains the top-level definition of the AWS CDK application.
- The /lib folder contains the test application stack definition, which defines the application described in the section above.
- /lib/functions folder contains the Lambda function execution code.
- /integ-tests contains the integration test stack, where you define and configure your test cases.
The repository is a typical AWS CDK application, except it has one additional directory for test case definitions. In the rest of this article, the authors will define the integration test in the /integ-tests/integ.sns-sqs-ddb.ts file and guide you through creating and executing an integration test.
Writing integration tests
An integration test should verify the expected behavior of your AWS CDK application. You can define an integration test for your application as follows:
1. Create a test stack from the CdkIntegTestsDemoStack definition and map it to your 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.”,
});
2. Define an integration test construct with a list of test cases. This construct offers the possibility to customize the behavior of the Integration Runner tool. For example, you can force the integration-runner to destroy the resources after running the test to force a cleanup.
Up.
// 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],
});
3. Add an assertion to validate the test results. In this example, you validate a single message flow from an Amazon SNS topic to an Amazon DynamoDB table. The assertion publishes a message object to the Amazon SNS topic using the AwsApiCall method. In the background, this method uses a custom CloudFormation resource supported by Lambda to make an Amazon SNS Publish API call from 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,
}),
})
4. Use the following helper method to combine the API calls. In this example, the second Amazon DynamoDB GetItem API call retrieves an item whose primary key equals the message ID. The result of the second API call is expected to match the message object, including the additional attribute added due to 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', },
},
}),
)
5. Because the application can take some time to pass the message, the authors run the assertion asynchronously by calling the waitForAssertions method. This means the Amazon DynamoDB GetItem API call is called at intervals until the expected result 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),
}),
);
6. The AwsApiCall method automatically adds the correct IAM permissions for both API calls to the AWS Lambda function. Since the Amazon SNS topic of the example application is encrypted using the 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],
});
Running integration tests
In this section, the authors show how to run an integration test for an introduced sample application using integ-runner to execute the test case and report the assertion results.
npm install
npm run build
Install and compile the project.
npm run integ-test
Run the following command to initiate test case execution with a list of options.
The directory option specifies which location integ-runner must recursively search for test definition files. The parallel regions option allows you to define a list of regions to run the tests. The developers set this to us-east-1 and ensured the AWS CDK load was previously executed in that region. The update on failure option allows integration tests to be restarted if the snapshot fails. A full list of available options can be found in the integ-runner Github repository.
Tip: if you want to preserve test stacks while programming for debugging, you can specify the no-clean option to preserve the test stack after running the test.
The integ-runner tool initially checks the snapshots of the integration test to determine if there have been any changes since the last execution. The snapshot verification fails because there are no previous snapshots for the first run. As a result, integ-runner starts execution of the integration test 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
Integ-runner generates two AWS CloudFormation stacks, as shown in Figure 3. The IntegrationTestStack contains resources from our sample application, an isolated application representing the stack under test. The DataFlowDefaultTestDeployAssert stack includes the resources required to execute the integration tests, as shown in Figure 4.
Ordering
Based on the specified RemovalPolicy, resources are automatically destroyed when the stack is removed. Some resources, such as Amazon DynamoDB tables, have the default RemovalPolicy set to Retain in AWS CDK. To set RemovalPolicy to Destroy for integration test resources, authors use 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 CLI integ-runner option, you must manually destroy the stacks. This can be done from the AWS console via AWS CloudFormation, as shown in Figure 5, or with the following command.
BASH
cdk destroy --all
To clean up the code repository compilation files, you can run the following script.
BASH
npm run clean
Applications
The AWS CDK integ-tests build is a valuable tool for defining and running automated integration tests for AWS CDK applications. In this article, the authors provide a practical code example showing how AWS CDK integration tests can be used to check an application's expected behavior after deployment to the cloud. You can use the techniques described in this guide to write your AWS CDK integration tests and improve the quality and reliability of your application releases.
For information on getting started with these constructs, see the documentation below.
Call to action
The Integ-runner and Integ-tests builds are experimental and subject to change. Version information for stable and experimental modules is available in the AWS CDK Github version information. As always, the developers look forward to receiving bug reports, new feature requests, and download requests in the aws-cdk GitHub repository to shape these alpha builds further based on your feedback.