| name | typescript-cdk-testing |
| description | AWS CDK testing patterns including snapshot tests, fine-grained assertions with Template.fromStack(), and normalization strategies. Use when writing or reviewing CDK infrastructure tests. |
CDK Testing Skill
Testing patterns for AWS CDK infrastructure in the Envilder project (src/iac/).
Tests live in tests/iac/ and run with Vitest.
When to Use
- Writing tests for new CDK stacks or constructs
- Adding assertions for specific resource properties
- Updating snapshots after intentional infrastructure changes
- Reviewing CDK test coverage
Test Architecture
Test Organization
Tests mirror the source structure:
tests/iac/
├── bin/
│ └── main.test.ts CDK app entry point test
├── lib/
│ ├── stacks/
│ │ ├── cloudfrontUrlRewrite.test.ts
│ │ ├── staticWebsiteStack.test.ts
│ │ └── __snapshots__/
│ └── utils/
│ └── cloudFormationUtils.test.ts
├── package.json
├── tsconfig.json
└── vitest.d.ts
Snapshot Testing (Primary Pattern)
~80% of CDK tests use snapshot testing to detect unintended infrastructure drift:
import { App } from "aws-cdk-lib";
import { Template } from "aws-cdk-lib/assertions";
describe("StaticWebsiteStack", () => {
it("Should_MatchSnapshot_When_StackIsSynthesized", () => {
const app = new App();
const stack = new StaticWebsiteStack(app, "TestStack", config);
const template = Template.fromStack(stack);
expect(template).toMatchSnapshot("staticWebsiteStackTest");
});
});
Snapshot Normalization
Remove non-deterministic values before snapshotting:
function normalizeTemplate(template: Template): object {
const json = template.toJSON();
for (const resource of Object.values(json.Resources ?? {})) {
if (resource.Properties?.Code?.S3Key) {
resource.Properties.Code.S3Key = "NORMALIZED";
}
if (resource.Properties?.SourceObjectKeys) {
resource.Properties.SourceObjectKeys = ["NORMALIZED"];
}
}
return json;
}
Normalize these properties:
S3Key: Lambda deployment artifact hash
SourceObjectKeys: Asset hashes
- Any
Fn::Join with account-specific values
Updating Snapshots
When infrastructure changes are intentional:
pnpm test -- -u
Rules:
- Review snapshot diffs carefully before approving
- Never update snapshots to suppress failures: understand the change first
- Commit updated snapshots alongside the code that caused the change
Fine-Grained Assertions (Secondary Pattern)
~20% of tests verify specific resource properties:
import { Template, Match } from "aws-cdk-lib/assertions";
describe("LambdaStack", () => {
it("Should_HaveCorrectTimeout_When_ApiLambdaIsCreated", () => {
const app = new App();
const stack = new LambdaStack(app, "TestStack", config);
const template = Template.fromStack(stack);
template.hasResourceProperties("AWS::Lambda::Function", {
Timeout: 30,
MemorySize: 1024,
Runtime: "provided.al2023",
});
});
it("Should_HaveDeadLetterQueue_When_SqsIsCreated", () => {
const app = new App();
const stack = new SqsStack(app, "TestStack", config);
const template = Template.fromStack(stack);
template.hasResourceProperties("AWS::SQS::Queue", {
RedrivePolicy: Match.objectLike({
maxReceiveCount: 3,
}),
});
});
});
Common Assertion Methods
| Method | Purpose |
|---|
template.hasResourceProperties(type, props) | Resource exists with properties |
template.hasResource(type, props) | Resource exists with full config |
template.resourceCountIs(type, count) | Exact count of resource type |
template.hasOutput(logicalId, props) | Stack output exists |
Match.objectLike({...}) | Partial match on nested objects |
Match.arrayWith([...]) | Array contains expected elements |
Match.stringLikeRegexp(pattern) | String matches regex |
Match.not(matcher) | Negation of any matcher |
Match.absent() | Property should not exist |
When to Use Which Pattern
| Scenario | Pattern |
|---|
| New stack or construct | Snapshot: captures full baseline |
| Specific security property (e.g., encryption) | Assertion: explicit check won't be missed |
| Resource count verification | Assertion: resourceCountIs |
| Cross-resource references | Assertion: verify Ref/Fn::GetAtt |
| Infrastructure refactor (same output) | Snapshot: confirms no drift |
| Config-driven resource creation | Snapshot + assertion for critical properties |
Test Naming
Follow the project Should_{Behavior}_When_{Condition} convention:
it("Should_MatchSnapshot_When_StackIsSynthesized", ...)
it("Should_HaveCorrectTimeout_When_ApiLambdaIsCreated", ...)
it("Should_CreateDeadLetterQueue_When_SqsHasMaxReceiveCount", ...)
it("Should_RestrictPublicAccess_When_S3BucketIsCreated", ...)
Test Data: Config Objects
Use realistic config objects that match the config-driven infrastructure pattern:
const testConfig: BackendStackConfig = {
lambdas: [{ name: "TestLambda", timeoutSeconds: 30, memorySizeMbs: 1024 }],
sqs: [{ name: "test-queue", maxReceiveCount: 3, deadLetter: true }],
s3: [{ bucketNameSuffix: "test-bucket" }],
fargates: [{ name: "TestService", cpu: 256, memoryLimitMiB: 512 }],
};
Commands
cd tests/iac && pnpm test
cd tests/iac && pnpm test -- -u
cd tests/iac && pnpm test -- lib/stacks/staticWebsiteStack.test.ts
cd tests/iac && pnpm test -- -t "Should_MatchSnapshot"
Anti-Patterns
| Anti-Pattern | Correct Approach |
|---|
| Snapshot without normalization | Normalize non-deterministic values (S3Key, hashes) |
| Testing CDK internals (construct tree) | Test the synthesized CloudFormation template |
| Hardcoded account/region in tests | Use test-specific App with mock env |
| Updating snapshots without reviewing diff | Always review what changed and why |
| Only snapshot tests for security-critical props | Add explicit assertions for encryption, IAM, SG rules |