DEV Community

Cover image for Testing your CDK Infrastructure with CDK Assertions
kreuzwerker
kreuzwerker

Posted on

Testing your CDK Infrastructure with CDK Assertions

Written by Gustavo Guimaraes

A new way to test your CDK IAC with ease

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.

Bild 1

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 Jest 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:

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"
   });
 }
}

Enter fullscreen mode Exit fullscreen mode

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”:

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"
     })
   );
 });
});
Enter fullscreen mode Exit fullscreen mode

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:

Bild 2.3

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.

Bild 3

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:

  1. 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.
template.hasResource('Foo::Bar', {
 Properties: { test: 'test' },
 DependsOn: [ 'other-resource' ],
});
Enter fullscreen mode Exit fullscreen mode
  1. 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
template.hasResourceProperties('Foo::Bar', {
   Foo: 'Bar',
   Qux: [ 'John', 'Luiz' ],
 });
Enter fullscreen mode Exit fullscreen mode
  1. allResourcesProperties: This method ensures that all types in the template respect the same specified type.
template.allResourcesProperties('Foo::Bar', {
 Foo: 'Bar',
 Qux: [ 'John', 'Luiz' ],
});
Enter fullscreen mode Exit fullscreen mode
  1. allResources: The behavior is similar to allResourcesProperties and it verifies the value in the whole type.
template.allResources('Foo::Bar', {
 Properties: { test: 'test' },
 DependsOn: [ 'other-resource' ],
});
Enter fullscreen mode Exit fullscreen mode
  1. resourceCountIs: It counts the number of resources in the Template that contains the type asserted and with the amount asserted.
template.resourceCountIs('Foo::Bar', 3);
Enter fullscreen mode Exit fullscreen mode
  1. 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.
template.resourcePropertiesCountIs('Foo::Bar', {
   Foo: 'Bar',
   Qux: [ 'John', 'Luiz' ],
 }, 1);
Enter fullscreen mode Exit fullscreen mode

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.

template.hasOutput('*', {
   Value: 'foo',
   Export: { Name: 'test' },
 });
Enter fullscreen mode Exit fullscreen mode

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:.

  1. 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.
template.hasResourceProperties('Foo::Bar', {
 Fred: Match.objectLike({
   test: 'test',
 }),
});
Enter fullscreen mode Exit fullscreen mode

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: :

  1. Match.anyValue: It will only check the existence of a value comparable to a “notNull” check.
template.hasResourceProperties('Foo::Bar', {
 Fred: Match.objectLike({
   test: Match.anyValue(),
 }),
});
Enter fullscreen mode Exit fullscreen mode
  1. Match.absent: It’s the opposite of the previous one, here we validate that the value is not present.
template.hasResourceProperties('Foo::Bar', {
 Fred: Match.objectLike({
   test: Match.absent(),
 }),
});
Enter fullscreen mode Exit fullscreen mode

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:

  1. 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.
template.hasResourceProperties('Foo::Bar', {
 Fred: Match.objectLike({
   test: Match.arrayWith(["test"]),
 }),
});
Enter fullscreen mode Exit fullscreen mode
  1. Match.arrayEquals: As compared to before, the “arrayEquals” method is stricter and it looks for an exact match.
template.hasResourceProperties('Foo::Bar', {
 Fred: Match.objectLike({
   test: Match.arrayWith(["test", "test1"]),
 }),
});
Enter fullscreen mode Exit fullscreen mode

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.

template.hasResourceProperties('Foo::Bar', {
 Fred: Match.objectLike({
   test: Match.stringLikeRegexp("tes*")
 }),
});
Enter fullscreen mode Exit fullscreen mode

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.

const testCapture = new Capture()
template.hasResourceProperties('Foo::Bar', {
 Fred: Match.objectLike({
   test: testCapture
 }),
});
expect(testCapture.asString).toBe("test")
Enter fullscreen mode Exit fullscreen mode

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.html. Hopefully this post gave you some pointers on how to successfully use CDK assertions.


Still questions? Get in touch: aws@kreuzwerker.de

Tags: AWS, Cloud, DevOps, CDK, Assertions, CloudFormation, Testing, TypeScript, Jest

Top comments (0)