Why and How to Use Snapshot Tests in AWS CDK

Jannik WempeJannik Wempe
6 min read

When I first encountered snapshot tests, I was skeptical. The concept seemed strange, and their benefits weren't immediately obvious. But after experiencing their value firsthand, I've become a convert – and here's why I think you should give them a chance.

What are Snapshot Tests (in AWS CDK)?

Snapshot tests capture the output of a system and save it as a reference "snapshot" file. During subsequent test runs, new snapshots are compared against this stored reference – any differences trigger a test failure. Think of it as taking a "photograph" of your system's output and comparing future changes against it.

Flowchart illustrating two processes: The first run involves creating a snapshot using CDK code and CloudFormation; the snapshot is then created. Subsequent runs involve CDK code and CloudFormation to compare with the snapshot, resulting in either a pass or fail outcome.

In AWS CDK, snapshot tests verify the CloudFormation templates that CDK generates from your infrastructure code. Whether you write CDK in TypeScript, Python, or other supported languages, your code ultimately synthesizes into CloudFormation templates. Snapshot testing ensures these templates remain consistent with your intentions.

Consider defining an S3 bucket in CDK – the snapshot test captures all generated CloudFormation properties, from basic bucket configurations to complex access policies. If a future code change modifies any of these properties unexpectedly, the snapshot test fails.

Snapshot tests are versatile – you can write them not only for individual constructs but also for entire CDK stacks, allowing you to verify your complete infrastructure definition in a single test.

Why Use Snapshot Tests in AWS CDK?

While snapshot tests might initially seem like a curious approach to testing infrastructure, they offer several compelling benefits that make them invaluable in AWS CDK development.

Low Development Effort

Writing snapshot tests requires minimal code – often just a few lines. Unlike traditional tests where you must write explicit assertions for each property, snapshot tests automatically capture and verify all aspects of your infrastructure. This makes them an excellent return on investment, providing broad coverage with minimal development overhead.

This is a snapshot test that I have written for my WebhooksStack.

import { App } from 'aws-cdk-lib'
import { Template } from 'aws-cdk-lib/assertions'
import { config } from '../../config'
import { EventBusStack } from '../event-bus/event-bus-stack'
import { WebhooksStack } from './webhooks-stack'

describe('WebhooksStack', () => {
    it('should match the snapshot', () => {
        // ARRANGE
        const app = new App()

        const eventBusStack = new EventBusStack(app, 'EventBus', config)
        const webhooksStack = new WebhooksStack(app, 'Webhooks', {
            ...config,
            eventBus: eventBusStack.eventBus,
        })

        // ACT
        const template = Template.fromStack(webhooksStack)

        // ASSERT
        expect(template.toJSON()).toMatchSnapshot()
    })
})

It just creates a CDK App, instantiates the WebhooksStack (and its dependencies), generates the CloudFormation template as JSON and asserts the snapshot to match the existing snapshot. It is all just a few lines of code.

Visual Infrastructure Changes

One of the most powerful features of snapshot tests is their integration with version control systems. When you modify your infrastructure code, the changes are reflected in the snapshot diff, making them clearly visible in pull requests. This visual representation makes code reviews more effective – your team can easily spot and discuss infrastructure modifications before they reach production.

This is a real-world screenshot I have taken from a simple AWS Lambda timeout adjustment, but I think it is sufficient to get the idea:

Screenshot from a GitHub pull request that shows a diff in an updated snapshot test file.

Guard Rails for Infrastructure Changes

Snapshot tests act as a safety net, ensuring that all infrastructure changes are intentional. If you're updating an S3 bucket policy but accidentally modify the bucket's encryption settings, the snapshot test will catch this unintended change. This protection is particularly valuable in large teams where multiple developers work on shared infrastructure code.

Refactoring with Confidence

When refactoring CDK constructs or reorganizing your infrastructure code, snapshot tests shine. They verify that your refactoring efforts maintain the same CloudFormation output, even if the CDK code structure changes significantly. This allows you to confidently modernize your infrastructure code without fear of accidental modifications to the deployed resources.

Additional Benefits

  • Documentation: Snapshots serve as living documentation of your infrastructure, showing exactly what CloudFormation resources your CDK code generates

  • Cross-Stack Validation: When testing entire stacks, snapshots help identify unintended changes in resource dependencies and cross-stack references

  • Learning Tool: For developers new to CDK, examining snapshot diffs helps them understand how their code changes translate to CloudFormation changes

  • Compliance Verification: Snapshots can help ensure that infrastructure changes comply with organizational standards by making all modifications visible and reviewable

How do you write snapshot tests in AWS CDK?

While writing snapshot tests is straightforward, there's an important caveat when testing infrastructure with Lambda functions. The generated CloudFormation template includes an S3Key property that contains a hash of your Lambda function's code. This means that every time you modify your Lambda code, even without changing the infrastructure, the snapshot test will fail because the S3Key hash changes. This isn't ideal when you only want to validate infrastructure changes rather than code changes.

Fortunately, snapshot serializers provide a solution to this problem. Testing frameworks like Jest and Vitest support custom snapshot serializers that can modify how the snapshots are generated. By implementing a serializer, you can strip out code-dependent elements like the S3Key (which contains a hash based on the code) from the CloudFormation template before it's saved as a snapshot. This ensures your tests only fail when there are actual infrastructure changes, not just Lambda code updates.

// ./setup-after-env.ts

// include this in `setupFilesAfterEnv` in the Jest config.

import 'aws-sdk-client-mock-jest'

import { config } from '../../src/config'

const bucketMatch = new RegExp(
    `cdk-[0-9a-z]{9}-assets-${config.env.account}-${config.env.region}`,
)
const assetMatch = /[0-9a-f]{64}\.zip/

/**
 * This is a custom snapshot serializer for the CDK.
 * It substitutes the bucket and asset zip parts with [ASSET BUCKET] and [ASSET ZIP] respectively.
 *
 * This ensures that the snapshot stays the same on asset changes.
 *
 * @see https://blog.bigbandsinger.dev/robust-cdk-snapshot-testing-with-snapshot-serializers#heading-making-snapshots-less-fragile-ftw
 */
expect.addSnapshotSerializer({
    test: (val) =>
        typeof val === 'string' && (val.match(bucketMatch) != null || val.match(assetMatch) != null),
    print: (val) => {
        // Substitute both the bucket part and the asset zip part
        let sval = `${val}`
        sval = sval.replace(bucketMatch, '[ASSET BUCKET]')
        sval = sval.replace(assetMatch, '[ASSET ZIP]')
        return `"${sval}"`
    },
})

I should mention that I borrowed this serializer code from another blog post – shout out to whoever wrote it! Sadly, the original link isn't working anymore as I write this, but hey, credit where credit is due.

What Does My AWS CDK and Snapshot Tests Workflow Look Like?

After initially creating your snapshot files (by running jest --testMatch '**/*.snapshot.test.ts' you will have to run jest --testMatch '**/*.snapshot.test.ts -u' (notice the -u for “update“) to update them on subsequent changes to the infrastructure.

💡
I put my snapshot tests in files with the suffix .snapshot.test.tswhich allows me to only run or update the snapshot tests by targeting them with the testMatch argument.

Here's how I use snapshot tests in my workflow: When I modify infrastructure code, I first run the tests locally to generate updated snapshots. I review these changes carefully, and if they match my intentions, I commit both the code and updated snapshots together. Then I push to GitHub and create a PR. The CI pipeline runs the snapshot tests again, verifying that everything matches – this catches any unintended changes before they make it to production.

Conclusion

Snapshot tests have become an essential part of my CDK development workflow. While they might seem unusual at first, their ability to catch unintended infrastructure changes with minimal effort makes them incredibly valuable. Combined with the custom serializer to handle Lambda code changes, they provide just the right level of protection without getting in the way.

If you haven't tried snapshot testing in your CDK projects yet, I encourage you to give it a shot. The small upfront investment in setting them up will pay off in confident infrastructure changes and more effective code reviews.

40
Subscribe to my newsletter

Read articles from Jannik Wempe directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Jannik Wempe
Jannik Wempe

Platform Engineering Lead @hashnode Serverless and Frontend Enthusiast 🤓 Into AWS, TypeScript, React, Svelte and upcoming trends… ☁️ AWS Community Builder Serverless Coding is not only a job – it's a passion. Based in Hamburg 🇩🇪