CDK Testing with Jest: AWS CDK and CDKTF Test Strategies
AWS CDK (Cloud Development Kit) lets you define AWS infrastructure with TypeScript, Python, Java, or Go. Because CDK produces CloudFormation templates, testing means asserting on the generated template — and CDK provides a first-class testing library for exactly this.
CDK Testing Philosophy
CDK tests don't deploy real infrastructure during the test run. Instead, they synthesize a CloudFormation template from your CDK app and assert on that template's contents. This means:
- No AWS credentials needed for unit tests
- Tests run in milliseconds — no cloud API calls
- Full CloudFormation template is inspectable
- Can be run locally and in CI without any setup
The CDK assertions library provides the tooling.
Setup
# CDK app dependencies
npm install aws-cdk-lib constructs
<span class="hljs-comment"># Testing dependencies
npm install --save-dev jest ts-jest @types/jest @aws-cdk/assertions
<span class="hljs-comment"># Or, for CDK v2 (assertions is built in):
npm install --save-dev jest ts-jest @types/jestjest.config.js:
module.exports = {
testEnvironment: "node",
roots: ["<rootDir>/test"],
testMatch: ["**/*.test.ts"],
transform: {
"^.+\\.tsx?$": "ts-jest"
},
};Your CDK Stack
// lib/web-stack.ts
import * as cdk from "aws-cdk-lib";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as ecs from "aws-cdk-lib/aws-ecs";
import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2";
export interface WebStackProps extends cdk.StackProps {
environment: string;
containerImage: string;
desiredCount: number;
}
export class WebStack extends cdk.Stack {
public readonly loadBalancerDns: cdk.CfnOutput;
constructor(scope: cdk.App, id: string, props: WebStackProps) {
super(scope, id, props);
const vpc = new ec2.Vpc(this, "Vpc", {
maxAzs: 2,
natGateways: props.environment === "production" ? 2 : 1,
});
const cluster = new ecs.Cluster(this, "Cluster", { vpc });
const taskDef = new ecs.FargateTaskDefinition(this, "TaskDef", {
cpu: props.environment === "production" ? 512 : 256,
memoryLimitMiB: props.environment === "production" ? 1024 : 512,
});
taskDef.addContainer("app", {
image: ecs.ContainerImage.fromRegistry(props.containerImage),
portMappings: [{ containerPort: 8080 }],
environment: {
NODE_ENV: props.environment,
},
logging: ecs.LogDrivers.awsLogs({ streamPrefix: "app" }),
});
const service = new ecs.FargateService(this, "Service", {
cluster,
taskDefinition: taskDef,
desiredCount: props.desiredCount,
assignPublicIp: false,
});
const alb = new elbv2.ApplicationLoadBalancer(this, "ALB", {
vpc,
internetFacing: true,
});
const listener = alb.addListener("Listener", { port: 80 });
listener.addTargets("Target", {
port: 8080,
targets: [service],
healthCheck: {
path: "/health",
interval: cdk.Duration.seconds(30),
},
});
this.loadBalancerDns = new cdk.CfnOutput(this, "LoadBalancerDns", {
value: alb.loadBalancerDnsName,
});
// Tags
cdk.Tags.of(this).add("Environment", props.environment);
cdk.Tags.of(this).add("ManagedBy", "CDK");
}
}Fine-Grained Assertions
The Template class from aws-cdk-lib/assertions is the main testing tool:
// test/web-stack.test.ts
import * as cdk from "aws-cdk-lib";
import { Template, Match } from "aws-cdk-lib/assertions";
import { WebStack } from "../lib/web-stack";
describe("WebStack", () => {
let app: cdk.App;
let stack: WebStack;
let template: Template;
beforeEach(() => {
app = new cdk.App();
stack = new WebStack(app, "TestStack", {
environment: "test",
containerImage: "my-app:latest",
desiredCount: 1,
});
template = Template.fromStack(stack);
});
describe("ECS configuration", () => {
it("creates a Fargate task definition", () => {
template.hasResourceProperties("AWS::ECS::TaskDefinition", {
RequiresCompatibilities: ["FARGATE"],
NetworkMode: "awsvpc",
});
});
it("allocates reduced resources in non-production", () => {
template.hasResourceProperties("AWS::ECS::TaskDefinition", {
Cpu: "256",
Memory: "512",
});
});
it("sets NODE_ENV environment variable", () => {
template.hasResourceProperties("AWS::ECS::TaskDefinition", {
ContainerDefinitions: Match.arrayWith([
Match.objectLike({
Environment: Match.arrayWith([
{ Name: "NODE_ENV", Value: "test" },
]),
}),
]),
});
});
it("exposes port 8080", () => {
template.hasResourceProperties("AWS::ECS::TaskDefinition", {
ContainerDefinitions: Match.arrayWith([
Match.objectLike({
PortMappings: [{ ContainerPort: 8080, Protocol: "tcp" }],
}),
]),
});
});
});
describe("Load balancer", () => {
it("creates an internet-facing ALB", () => {
template.hasResourceProperties("AWS::ElasticLoadBalancingV2::LoadBalancer", {
Scheme: "internet-facing",
Type: "application",
});
});
it("configures health check on /health path", () => {
template.hasResourceProperties("AWS::ElasticLoadBalancingV2::TargetGroup", {
HealthCheckPath: "/health",
HealthCheckIntervalSeconds: 30,
});
});
});
describe("Production configuration", () => {
let prodTemplate: Template;
beforeEach(() => {
const prodStack = new WebStack(app, "ProdStack", {
environment: "production",
containerImage: "my-app:v1.0.0",
desiredCount: 3,
});
prodTemplate = Template.fromStack(prodStack);
});
it("allocates more CPU in production", () => {
prodTemplate.hasResourceProperties("AWS::ECS::TaskDefinition", {
Cpu: "512",
Memory: "1024",
});
});
it("uses 2 NAT gateways in production", () => {
// Production = 2 AZs with 2 NAT gateways = 2 NAT Gateway resources
prodTemplate.resourceCountIs("AWS::EC2::NatGateway", 2);
});
});
describe("Resource counts", () => {
it("creates exactly one VPC", () => {
template.resourceCountIs("AWS::EC2::VPC", 1);
});
it("creates exactly one ECS cluster", () => {
template.resourceCountIs("AWS::ECS::Cluster", 1);
});
it("creates a CloudWatch log group", () => {
template.resourceCountIs("AWS::Logs::LogGroup", 1);
});
});
describe("Tags", () => {
it("tags all resources with environment", () => {
// Tags propagate via CDK - check a key resource
template.hasResourceProperties("AWS::ECS::Service", {
Tags: Match.arrayWith([
{ Key: "Environment", Value: "test" },
{ Key: "ManagedBy", Value: "CDK" },
]),
});
});
});
});Snapshot Testing
CDK also supports snapshot testing — generating a snapshot of the full CloudFormation template and failing if it changes unexpectedly:
import { Template } from "aws-cdk-lib/assertions";
it("matches snapshot", () => {
const app = new cdk.App();
const stack = new WebStack(app, "SnapshotStack", {
environment: "test",
containerImage: "my-app:latest",
desiredCount: 1,
});
const template = Template.fromStack(stack);
expect(template.toJSON()).toMatchSnapshot();
});First run creates __snapshots__/web-stack.test.ts.snap. Subsequent runs compare against it.
When to use snapshots:
- After intentional changes, update with
jest --updateSnapshot - Good for catching unintended template drift
When NOT to use snapshots:
- They don't tell you why something changed — large template diffs are hard to read
- Don't replace fine-grained assertions — use both
Testing CDKTF (Terraform CDK)
CDKTF uses CDK constructs to generate Terraform configuration. Testing works similarly:
// lib/database-stack.ts
import { Construct } from "constructs";
import { TerraformStack } from "cdktf";
import { AwsProvider } from "@cdktf/provider-aws/lib/provider";
import { DbInstance } from "@cdktf/provider-aws/lib/db-instance";
import { DbSubnetGroup } from "@cdktf/provider-aws/lib/db-subnet-group";
export class DatabaseStack extends TerraformStack {
constructor(scope: Construct, id: string, config: {
environment: string;
instanceClass: string;
multiAz: boolean;
}) {
super(scope, id);
new AwsProvider(this, "aws", { region: "us-east-1" });
const subnetGroup = new DbSubnetGroup(this, "SubnetGroup", {
name: `${config.environment}-db-subnet-group`,
subnetIds: [], // In real code, these come from VPC lookups
tags: { Environment: config.environment },
});
new DbInstance(this, "Database", {
identifier: `${config.environment}-postgres`,
engine: "postgres",
engineVersion: "15.4",
instanceClass: config.instanceClass,
allocatedStorage: 20,
dbSubnetGroupName: subnetGroup.name,
multiAz: config.multiAz,
storageEncrypted: true,
tags: { Environment: config.environment },
});
}
}// test/database-stack.test.ts
import { Testing } from "cdktf";
import { DatabaseStack } from "../lib/database-stack";
describe("DatabaseStack", () => {
it("enables multi-AZ in production", () => {
const app = Testing.app();
const stack = new DatabaseStack(app, "test", {
environment: "production",
instanceClass: "db.t3.medium",
multiAz: true,
});
const synth = Testing.synth(stack);
const config = JSON.parse(synth);
expect(config.resource.aws_db_instance).toBeDefined();
const dbInstance = Object.values(config.resource.aws_db_instance)[0] as any;
expect(dbInstance.multi_az).toBe(true);
});
it("enables storage encryption", () => {
const app = Testing.app();
const stack = new DatabaseStack(app, "test", {
environment: "test",
instanceClass: "db.t3.micro",
multiAz: false,
});
const synth = Testing.synth(stack);
const config = JSON.parse(synth);
const dbInstance = Object.values(config.resource.aws_db_instance)[0] as any;
expect(dbInstance.storage_encrypted).toBe(true);
});
});Running CDK Tests in CI
name: CDK Tests
on:
pull_request:
paths:
- 'infrastructure/**'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
working-directory: infrastructure/
- name: Run tests
run: npx jest
working-directory: infrastructure/
- name: CDK Synth (verify it generates valid templates)
run: npx cdk synth --all
working-directory: infrastructure/
env:
CDK_DEFAULT_ACCOUNT: "000000000000"
CDK_DEFAULT_REGION: "us-east-1"Note that cdk synth doesn't require real AWS credentials if you handle account/region via environment variables.
Summary
| Test Type | API | Speed | Credentials |
|---|---|---|---|
| Fine-grained assertions | Template.hasResourceProperties() |
Milliseconds | None |
| Resource count checks | Template.resourceCountIs() |
Milliseconds | None |
| Snapshot tests | expect(template.toJSON()).toMatchSnapshot() |
Milliseconds | None |
| CDK synth | cdk synth |
Seconds | Optional |
| Integration deploy | Automation API / actual deploy | Minutes | Required |
CDK's testing library makes infrastructure testing genuinely fast and developer-friendly. Fine-grained assertions on Template catch the most common mistakes — wrong instance types, missing security groups, incorrect tag propagation — before anything touches a real AWS account.
For verifying that your CDK-deployed infrastructure serves real traffic correctly, HelpMeTest provides continuous end-to-end monitoring after every deployment.