--- 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: ```txt 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: ```typescript import { App } from "aws-cdk-lib"; import { Template } from "aws-cdk-lib/assertions"; describe("StaticWebsiteStack", () => { it("Should_MatchSnapshot_When_StackIsSynthesized", () => { // Arrange const app = new App(); const stack = new StaticWebsiteStack(app, "TestStack", config); // Act const template = Template.fromStack(stack); // Assert expect(template).toMatchSnapshot("staticWebsiteStackTest"); }); }); ``` ### Snapshot Normalization Remove non-deterministic values before snapshotting: ```typescript 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: ```bash 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: ```typescript import { Template, Match } from "aws-cdk-lib/assertions"; describe("LambdaStack", () => { it("Should_HaveCorrectTimeout_When_ApiLambdaIsCreated", () => { // Arrange const app = new App(); const stack = new LambdaStack(app, "TestStack", config); const template = Template.fromStack(stack); // Act & Assert template.hasResourceProperties("AWS::Lambda::Function", { Timeout: 30, MemorySize: 1024, Runtime: "provided.al2023", }); }); it("Should_HaveDeadLetterQueue_When_SqsIsCreated", () => { // Arrange const app = new App(); const stack = new SqsStack(app, "TestStack", config); const template = Template.fromStack(stack); // Act & Assert 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: ```typescript 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: ```typescript 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 ```bash # Run all CDK tests cd tests/iac && pnpm test # Update snapshots (after intentional changes only) cd tests/iac && pnpm test -- -u # Run specific test file cd tests/iac && pnpm test -- lib/stacks/staticWebsiteStack.test.ts # Filter by test name 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 |