Snapshot Testing in AWS CDK

Along with multi language support, reliable documentation and steady upgrades, AWS CDK has its own testing constructs as well. Tests in CDK can be categorised into two types: Snapshot and Fine-grained assertions. Snapshot tests tend to be more simplified in terms of implementation as well as maintenance.

In this article we will take a deeper look at implementing a Snapshot test in AWS CDK.

 

Table of contents

 

What is Snapshot Testing

From Jest Documentation:

A typical snapshot test case renders a UI component, takes a snapshot, then compares it to a reference snapshot file stored alongside the test. The test will fail if the two snapshots do not match: either the change is unexpected, or the reference snapshot needs to be updated to the new version of the UI component.

Simply put, Snapshot tests compare a previously stored “golden master” output with the output generated with current configuration. Any differences with the current output would be highlighted. If the changes are intentional, the master output can be updated with the latest developments. Snapshot testing can be considered to fall under regression testing.

In case of AWS CDK, the “golden master” is nothing but the synthesized template. From AWS Documentation:

Snapshot tests test the synthesized AWS CloudFormation template against a previously-stored baseline template or "master." Snapshot tests let you refactor freely, since you can be sure that the refactored code works exactly the same way as the original. If the changes were intentional, you can accept a new baseline for future tests.

Implementing snapshot is quite simple and they can be extremely useful for detecting and correcting any misconfigurations in the template before deployment. From a maintenance point of view, snapshot tests are very convenient as they only involve updating the master template whenever intentional changes are introduced.

However, upgrades to CDK may introduce changes in the synthesized template, generating unnecessary warnings in tests even though no configuration changes were expected. This issue could be resolved by sticking to a constant CDK version or updating the master template whenever newer changes are introduced in CDK templates. These drawbacks make detailed assertion unit tests more reliable than snapshots.

Despite such drawbacks, snapshot tests are often preferred due to their ease of execution and low maintenance. Especially in scenarios where detailed testing is not a top priority.

As I already mentioned, they are extremely easy to implement. Let’s have a look.

 

Example Stack

In this stack, we are going to create a simple S3 bucket. The bucket would be configured with some basic properties like versioning, bucket name, encryption, lifecycle rules and so on. For introductory testing, just applying a bucket name property would be enough. Note that we would be working on CDK v2 throughout this example, and our language of choice would be Typescript.

The configured prerequisites on my system are as follows:

  • Node: v16.14.2
  • Npm: v8.5.0
  • aws-cli: v2
  • aws-cdk: v2.25.0
  • Typescript: v4.3.5

First, let's initialize our project.

$ mkdir cdk-s3
$ cd cdk-s3
$ cdk init app --language typescript

Our example CdkS3Stack from cdk-s3/cdk-s3-stack.ts:

import { Stack, StackProps } from 'aws-cdk-lib';
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { aws_s3 as s3 } from 'aws-cdk-lib';

export class CdkS3Stack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    new s3.Bucket(this, "snapshotdemobucket", {
      versioned: true,
      bucketName: "snapshot-demo-bucket",
      objectOwnership: s3.ObjectOwnership.BUCKET_OWNER_PREFERRED,
      encryption: s3.BucketEncryption.S3_MANAGED,
      lifecycleRules: [
        {
          expiration: cdk.Duration.days(30)
        }
      ],
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      removalPolicy: cdk.RemovalPolicy.DESTROY
    });
  }
}

Try synthesizing the template with a cdk synth to generate the CloudFormation template. The stack is now ready for unit tests to be applied.

 

Testing the Code

Now we move on to create the testing code. Automated tests in CDK are implemented using the AWS CDK’s assertion module. In addition, for TypeScript and JavaScript, Jest testing framework is used whereas Pytest is preferred for Python.

The first step is to add the Jest module object in your project. Modify cdk-s3/package.json by adding the following Jest object:

"jest": {
    "moduleFileExtensions": ["js"]
  }

Your cdk-s3/package.json should look like:

{
  "name": "cdk-s3",
  . . .
  "scripts": {
    . . .
  },
  "devDependencies": {
    . . .
    "@types/jest": "^27.5.0",
    "aws-cdk": "2.25.0",
    . . .
  },
  "dependencies": {
    "aws-cdk-lib": "2.25.0",
    "constructs": "^10.0.0",
    "source-map-support": "^0.5.21"
  },
  "jest": {
    "moduleFileExtensions": ["js"]
  }
}

Additionally, you can also run the following command to update the jest library to its latest version:

npm install --save-dev jest @types/jest


We move on to write our test code in cdk-s3/test/cdk-s3-test.ts. Get rid of the already existing boilerplate code and create a snapshot test:

import { Template } from "aws-cdk-lib/assertions"
import { App } from 'aws-cdk-lib/core';
import { CdkS3Stack } from '../lib/cdk-s3-stack';

// Snapshot test
test("S3 Bucket created", () => {
  // Instantiate the cdk app
  const app = new App();

  // Create S3 Stack
  const cdkS3Stack = new CdkS3Stack(app, "CdkS3Stack");

  // Prepare the stack for assertions
  const template = Template.fromStack(cdkS3Stack);

  // Match with Snapshot
  expect(template.toJSON()).toMatchSnapshot();
});

Here we create a stack and derive its template using the assertions module. We then tests the generated snapshots using the expect().toMatchSnapshot() function.



NOTE:

If you are using CDK V1, you would need to use the SynthUtils module from @aws-cdk/assert library. In that case, snapshot would be matched using:

expect(SynthUtils.toCloudFormation(cdkS3Stack)).toMatchSnapshot();


All set. Let's try to test our code with npm test:

$ npm test

> cdk-s3@0.1.0 test
> jest

 PASS  test/cdk-s3.test.ts (7.03 s)
  √ S3 Bucket created (41 ms)

 › 1 snapshot written.
Snapshot Summary
 › 1 snapshot written from 1 test suite.

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   1 written, 1 total
Time:        7.892 s
Ran all test suites.

Here one snapshots gets created and the test would be passed as expected. You can view the snapshot in the cdk-s3/test/__snapshots__ directory. Notice that it would be very similar to the synthesized template.


Let’s see how the test results look like after some changes are introduced in the code:

. . .
      bucketName: "snapshot-demo-bucket",
      objectOwnership: s3.ObjectOwnership.BUCKET_OWNER_PREFERRED,
      encryption: s3.BucketEncryption.S3_MANAGED,
      lifecycleRules: [
        {
          expiration: cdk.Duration.days(25)
        }
      ],
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
. . .

The expiration days have been changed from '30' to '25'. Here are the test results:

$ npm test

> cdk-s3@0.1.0 test
> jest

 FAIL  test/cdk-s3.test.ts (20.652 s)
  × S3 Bucket created (49 ms)
                                                                                                                                                                                                        
  ● S3 Bucket created                                                                                                                                                                                   
                                                                                                                                                                                                        
    expect(received).toMatchSnapshot()

    Snapshot name: `S3 Bucket created 1`

    - Snapshot  - 1
    + Received  + 1

    @@ -21,11 +21,11 @@
              },
              "BucketName": "snapshot-demo-bucket",
              "LifecycleConfiguration": Object {
                "Rules": Array [
                  Object {
    -               "ExpirationInDays": 30,
    +               "ExpirationInDays": 25,
                    "Status": "Enabled",
                  },
                ],
              },
              "OwnershipControls": Object {

      15 |
      16 |   // Match with Snapshot
    > 17 |   expect(template.toJSON()).toMatchSnapshot();
         |                             ^
      18 | });

      at Object.<anonymous> (test/cdk-s3.test.ts:17:29)

 › 1 snapshot failed.
Snapshot Summary                                                                                                                                                                                        
 › 1 snapshot failed from 1 test suite. Inspect your code changes or run `npm test -- -u` to update them.                                                                                               

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   1 failed, 1 total
Time:        21.369 s
Ran all test suites.

The test fails clearly highlighting the differences between the master template and current configuration. This helps detect crucial errors in the configuration before proceeding with deployment.


When the changes are intentional, we use the npm test -- -u command to update the snapshot:

$ npm test -- -u

> cdk-s3@0.1.0 test
> jest "-u"

 PASS  test/cdk-s3.test.ts (6.57 s)
  √ S3 Bucket created (46 ms)
                                                                                                                                                                                                        
 › 1 snapshot updated.                                                                                                                                                                                  
Snapshot Summary                                                                                                                                                                                        
 › 1 snapshot updated from 1 test suite.                                                                                                                                                                

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   1 updated, 1 total
Time:        6.782 s, estimated 8 s
Ran all test suites.

The snapshot gets updated and the tests cases pass. Here we are using -u which is the --updateSnapshot flag from Jest.

That is all there is to Snapshot testing in AWS CDK. Now we know how to write a snapshot test as well how to generate and update a snapshot.

 

Conclusion

Snapshot tests are reliable when the goal is to compare and make sure the right is output is being generated. They are also extremely easy to implement and maintain. It’s easier to detect any unwanted modifications in the template before deployment using snapshot tests.

Although we cannot test details of individual elements of the template using Snapshots. For more detailed testing, Fine-grained assertions are preferred.

The upcoming blog would cover implementation of fine-grained assertions in detail, so stay tuned.

 

Author:

Rahul Raje

JTP Co., Ltd.