PCG logo
Article

Testing your CDK Infrastructure with CDK Assertions

Introduction

The “Test for Infrastructure” concept is still new for quite a few cloud developers. This is because before IaC (short for “Infrastructure as Code”) existed, the only way to test your infrastructure was to check the created resources manually. This manual process was complicated and time-consuming, as it required you to set up a whole environment, which is only used to validate that the state of the resources is as expected. Moreover, automated testing tools didn’t exist.

With IaC, things became much simpler. There is now an abstraction of resources, which can be used to validate the “code” before deployment. One of those tools used for the same is CDK Assertions.

CDK Assertions - First look

CDK is an Infrastructure As Code development tool powered by AWS that creates our resources using programming languages familiar to the development team, such as TypeScript or Java. This way, a developer doesn’t have to learn any new programming/scripting language, and is able to quickly and reliably deploy new resources or modify existing resources.

Internally, CDK generates a CloudFormation template that is used to deploy resources. This is where CDK Assertions can be used to ensure that the resources are created in the desired state and have all the attributes needed to reach the expected result.

image-39bf543f3ff3

CDK has been offering CDK Assertions since version 2 was released at the end of 2021 and the support for version 1 was later added. It has the ability to look at the template generated by CDK and evaluate it against a set of assertions.

The following sections will demonstrate how to use CDK Assertions by using basic TypeScript examples. We will also use JestExternal Link as the testing framework.

The First Step

Let’s start with a single template containing one S3 bucket. This is one of the simplest CDK constructs that we can create:

Code Copied!copy-button
import {Stack} from 'aws-cdk-lib';
import {Bucket} from 'aws-cdk-lib/aws-s3';
import {Construct} from 'constructs';

export class S3Bucket extends Stack {

 constructor(scope: Construct, id: string) {
   super(scope, id);

   new Bucket(this, 'Bucket', {
       bucketName: "bucket-example"
   });
 }
}

Now let’s write a simple CDK Assertion, asserting that our infrastructure code defines the creation of an s3 bucket with the name “bucket-example”:

Code Copied!copy-button
import * as cdk from 'aws-cdk-lib';
import {Match, Template} from 'aws-cdk-lib/assertions';
import {S3Bucket} from '../lib/s3-creation';

let app: cdk.App, stack: cdk.Stack, template: Template;

beforeAll(() => {
 app = new cdk.App();
 const stack =  new S3Bucket(app, 'bucket')
 template = Template.fromStack(stack);
});

describe('S3 bucket', () => {
 it('Should have a S3 bucket with the name ‘bucket-example’ present', () => {
   template.hasResourceProperties('AWS::S3::Bucket',
     Match.objectLike({
       BucketName: "bucket-example"
     })
   );
 });
});

Let’s breakdown what the test class above is doing:

  1. The statement “beforeAll” is part of the Jest framework. It will be executed before every test and create the S3 bucket stack and save it in memory in the variable “template”.
  2. Once we have the “template”, we assert it with our first method “hasResourceProperties” that will look for the first parameter “AWS::S3::Bucket” inside our generated CloudFormation template.

Further, it will try to “Match” the object using the “objectLike” condition to verify if the parameters in the test are contained in the actual template by matching it with the expected one.We get a successful result when we run the test initially:

image-e0abd9f2bf8f

But if we accidentally change the bucket name to another name, for example “bucket-name-changed”, our test will fail and the console logs will inform the developer which condition was not met in our assertion.

image-83f1522aa2cc

You can see that in our assertion we expect “bucket-example”, but we receive a “failing” message, along with the necessary clue to fix the mistake or if it was intentionally changed, then to test the new value.

A Deeper Dive

Now that we know the basics of CDK Assertions we can try to understand more about some of the other functionalities offered by this tool:

Resources Matchers

First, let us have a look into what “template” methods are offered:

hasResource: A CloudFormation resource is composed of multiple different objects. The most important one from a test perspective is the “properties” that a resource has, but it is commonly needed to test the other ones as well. For this we can use certain special statements and this is one of them.

Code Copied!copy-button
template.hasResource('Foo::Bar', {
 Properties: { test: 'test' },
 DependsOn: [ 'other-resource' ],
});

hasResourceProperties: This method investigates the template and searches for the first sent parameter that is of the expected CloudFormation type. If the resource is found, it will continue the test, otherwise the assertion fails

Code Copied!copy-button
template.hasResourceProperties('Foo::Bar', {
   Foo: 'Bar',
   Qux: [ 'John', 'Luiz' ],
 });

allResourcesProperties: This method ensures that all types in the template respect the same specified type.

Code Copied!copy-button
template.allResourcesProperties('Foo::Bar', {
 Foo: 'Bar',
 Qux: [ 'John', 'Luiz' ],
});

allResources: The behavior is similar to allResourcesProperties and it verifies the value in the whole type.

Code Copied!copy-button
template.allResources('Foo::Bar', {
 Properties: { test: 'test' },
 DependsOn: [ 'other-resource' ],
});

resourceCountIs: It counts the number of resources in the Template that contains the type asserted and with the amount asserted.

Code Copied!copy-button
template.resourceCountIs('Foo::Bar', 3);

resourcePropertiesCountIs: Similar to resourceCountIs, this method asserts the number of resources from a specific type, but it allows for more filters and checks more for the types and properties too.

Code Copied!copy-button
template.resourcePropertiesCountIs('Foo::Bar', {
   Foo: 'Bar',
   Qux: [ 'John', 'Luiz' ],
 }, 1);

Output

Another important aspect of CDK Assertions is the ability to check the outputs which are exported from stacks to be utilized in other stacks.

Let’s see a common test of this function:

hasOutput: This method validates if an output exists from the template, and by using the wildcard(*) it ignores any specific ID and looks for all the potential matches. Instead of the wildcard, we can also specify the ID if we want.

Code Copied!copy-button
template.hasOutput('*', {
   Value: 'foo',
   Export: { Name: 'test' },
 });

Matchers

Matchers are the core functionality of CDK Assertions in my opinion. They offer a wide amount of options to validate how the template is validated.

Object Matchers:

We have two ways to validate an object:.

Match.objectLike: This matcher is non-strict in its implementation, and it’ll validate if the given parameter exists in the resource or not while ignoring other parameters.

Code Copied!copy-button
template.hasResourceProperties('Foo::Bar', {
 Fred: Match.objectLike({
   test: 'test',
 }),
});

In the above case, even if the resource under test has other parameters, they will be ignored and the test will pass.

  1. Match.objectEquals: This matcher performs a strict validation and the resource needs to perfectly match with the given parameters in the test.

Existence Checks

In certain cases we might not need to validate the specific contents of a value, but only assert if the value exists or not. For such cases, we have the following matchers: :

Match.anyValue: It will only check the existence of a value comparable to a “notNull” check.

Code Copied!copy-button
template.hasResourceProperties('Foo::Bar', {
 Fred: Match.objectLike({
   test: Match.anyValue(),
 }),
});

Match.absent: It’s the opposite of the previous one, here we validate that the value is not present.

Code Copied!copy-button
template.hasResourceProperties('Foo::Bar', {
 Fred: Match.objectLike({
   test: Match.absent(),
 }),
});

Matchers for arrays

There is often the need to validate arrays in any common programming language that we use. It’s not different when we talk about IaC, and for specifically this task we have certain Matchers:

Match.arrayWith: This method is utilized when we have an array with several items, but we only need to match if we have some matching items.

Code Copied!copy-button
template.hasResourceProperties('Foo::Bar', {
 Fred: Match.objectLike({
   test: Match.arrayWith(["test"]),
 }),
});

Match.arrayEquals: As compared to before, the “arrayEquals” method is stricter and it looks for an exact match.

Code Copied!copy-button
template.hasResourceProperties('Foo::Bar', {
 Fred: Match.objectLike({
   test: Match.arrayWith(["test", "test1"]),
 }),
});

Matchers for Strings

To test String values, you can test for exact matching values or use a regular expression with the following operation:

Match.StringLikeRegexp: Using this expression, we can validate a string with the use of wildcards like (*) or (|) to compare the expected value as part of a string or within a range.

Code Copied!copy-button
template.hasResourceProperties('Foo::Bar', {
 Fred: Match.objectLike({
   test: Match.stringLikeRegexp("tes*")
 }),
});

Capture

In some cases, we might need post validations that don’t just verify the content. For example, we get some return values in a list and we make a post validation on the size of the list or validate its naming convention. For this specific purpose we can use:

Capture: With Capture we can get the values from a key and afterwards assert them using the common test framework.

Code Copied!copy-button
const testCapture = new Capture()
template.hasResourceProperties('Foo::Bar', {
 Fred: Match.objectLike({
   test: testCapture
 }),
});
expect(testCapture.asString).toBe("test")

Conclusion

With the introduction of CDK Assertions we have a very powerful tool that helps us to abstract the complexity of the real resources. It allows us to ensure in great part that our IaC code will have the expected resources.

However, CDK Assertions aren’t a silver bullet. Mostly because they are unable to test the real resources, and only give a glimpse of the expected result; most importantly the connection between the resources and the parameters and their values that determine the resources.

For more information about this framework, I invite you to visit the official page in https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.assertions-readme.htmlExternal Link. Hopefully this post gave you some pointers on how to successfully use CDK assertions.


Services Used

Continue Reading

Article
AWS Lambda: Avoid these common pitfalls

It's a great offering to get results quickly, but like any good tool, it needs to be used correctly.

Learn more
Case Study
Financial Services
Cloud Migration
The VHV Group's Cloud Journey - Strategy for Success

How does an insurance company with more than 4,000 employees balance compliance, modernization, and cost efficiency?

Learn more
Case Study
Financial Services
DevOps
A KYC Archival System for a Digital Bank

Building a KYC archival Cloud platform for a digital bank to store customers’ KYC data.

Learn more
Case Study
Software
DevOps
Accounting Accelerates

What began as a start-up in their parents' basement has developed into a leading provider of cloud-based accounting and financial software within just a few years: sevDesk.

Learn more
See all

Let's work together

United Kingdom
Arrow Down