Testing CDK Applications with TypeScript
/ 4 min read
Series Navigation
- Part 1: Getting Started with Terraform CDK
- Part 2: Resource Management with CDK
- Part 3: Advanced TypeScript Patterns
- Part 4: Custom Constructs and Components
- Part 5: Testing CDK Applications (Current)
- Part 6: CI/CD for CDK Projects
Testing CDKTF Applications
Testing infrastructure code is crucial for maintaining reliability and preventing costly mistakes. Let’s explore different testing strategies for CDKTF applications.
Setting Up the Testing Environment
Test Configuration
module.exports = { testMatch: ['**/*.test.ts'], transform: { '^.+\\.tsx?$': 'ts-jest' }, moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], collectCoverage: true, coverageReporters: ['text', 'lcov'], coverageDirectory: 'coverage', testEnvironment: 'node'};Test Utilities
import { Testing } from "cdktf";import { TerraformStack } from "cdktf";
export function createTestStack(): TerraformStack { const app = Testing.app(); return new TerraformStack(app, "test-stack");}
export function synthesizeStack(stack: TerraformStack) { return Testing.synth(stack);}Unit Testing
Testing Custom Constructs
import { Testing } from "cdktf";import { VpcConstruct } from "./vpc-construct";
describe("VpcConstruct", () => { test("creates VPC with correct CIDR", () => { const stack = createTestStack();
new VpcConstruct(stack, "test-vpc", { cidrBlock: "10.0.0.0/16", enableDnsHostnames: true, });
const synthStack = synthesizeStack(stack); expect(synthStack).toHaveResourceWithProperties("aws_vpc", { cidr_block: "10.0.0.0/16", enable_dns_hostnames: true, }); });
test("creates public and private subnets", () => { const stack = createTestStack();
const vpc = new VpcConstruct(stack, "test-vpc", { cidrBlock: "10.0.0.0/16", });
const synthStack = synthesizeStack(stack); expect(vpc.publicSubnets).toHaveLength(2); expect(vpc.privateSubnets).toHaveLength(2); expect(synthStack).toHaveResource("aws_subnet"); });});Testing Resource Properties
describe("SecurityGroupConstruct", () => { test("creates security group with correct rules", () => { const stack = createTestStack();
new SecurityGroupConstruct(stack, "test-sg", { vpcId: "vpc-123", ingressRules: [{ fromPort: 80, toPort: 80, protocol: "tcp", cidrBlocks: ["0.0.0.0/0"], }], });
const synthStack = synthesizeStack(stack); expect(synthStack).toHaveResourceWithProperties("aws_security_group_rule", { type: "ingress", from_port: 80, to_port: 80, protocol: "tcp", cidr_blocks: ["0.0.0.0/0"], }); });});Snapshot Testing
Infrastructure Snapshot Tests
describe("Infrastructure Snapshot", () => { test("infrastructure matches snapshot", () => { const stack = createTestStack();
new WebApplication(stack, "web-app", { environment: "test", instanceType: "t2.micro", minSize: 1, maxSize: 3, vpcId: "vpc-123", subnetIds: ["subnet-1", "subnet-2"], });
const synthStack = synthesizeStack(stack); expect(JSON.stringify(synthStack, null, 2)).toMatchSnapshot(); });});Integration Testing
Testing Resource Dependencies
describe("DatabaseCluster Integration", () => { test("creates RDS cluster with correct dependencies", async () => { const stack = createTestStack();
const dbCluster = new DatabaseCluster(stack, "test-db", { engine: "postgres", instanceClass: "db.t3.medium", masterUsername: "admin", masterPassword: "password123", vpcId: "vpc-123", subnetIds: ["subnet-1", "subnet-2"], });
const synthStack = synthesizeStack(stack);
// Test resource creation order expect(synthStack).toHaveResource("aws_db_subnet_group"); expect(synthStack).toHaveResource("aws_rds_cluster_parameter_group"); expect(synthStack).toHaveResource("aws_security_group"); expect(synthStack).toHaveResource("aws_rds_cluster");
// Test dependencies const cluster = synthStack.getResource("aws_rds_cluster", dbCluster.cluster.id); expect(cluster.dependsOn).toContain( synthStack.getResource("aws_db_subnet_group", dbCluster.subnetGroup.id) ); });});Testing Utilities and Helpers
Custom Matchers
expect.extend({ toHaveTag(received: any, key: string, value: string) { const tags = received.tags || {}; const pass = tags[key] === value; return { message: () => `expected resource to have tag ${key}=${value}`, pass, }; },});
// Usage in teststest("resources have correct tags", () => { const stack = createTestStack();
const vpc = new VpcConstruct(stack, "test-vpc", { cidrBlock: "10.0.0.0/16", tags: { Environment: "test", }, });
expect(vpc).toHaveTag("Environment", "test");});Test Data Generators
export class TestDataGenerator { static generateVpcConfig(overrides: Partial<VpcConfig> = {}) { return { cidrBlock: "10.0.0.0/16", enableDnsHostnames: true, enableDnsSupport: true, tags: { Environment: "test", }, ...overrides, }; }
static generateDbConfig(overrides: Partial<DbConfig> = {}) { return { engine: "postgres", instanceClass: "db.t3.medium", masterUsername: "admin", masterPassword: "password123", ...overrides, }; }}Testing Best Practices
- Test Organization
- Group tests logically
- Use descriptive test names
- Maintain test independence
describe("WebApplication", () => { describe("load balancer", () => { test("creates ALB with correct configuration"); test("configures health checks"); test("sets up listener rules"); });
describe("auto scaling group", () => { test("sets correct capacity limits"); test("uses specified launch template"); test("configures scaling policies"); });});- Test Coverage
- Aim for high coverage
- Focus on critical paths
- Test edge cases
module.exports = { coverageThreshold: { global: { branches: 80, functions: 80, lines: 80, statements: 80, }, },};- Mocking and Stubs
- Mock external services
- Stub complex computations
- Use test doubles effectively
jest.mock("./aws-client", () => ({ getLatestAmiId: jest.fn().mockResolvedValue("ami-12345"), describeVpc: jest.fn().mockResolvedValue({ VpcId: "vpc-123", CidrBlock: "10.0.0.0/16", }),}));Next Steps
In Part 6: CI/CD for CDK Projects, we’ll explore how to set up continuous integration and deployment pipelines for your CDKTF applications.