AWS CDK Integration Testing with integ-tests-alpha: Real Deployment Assertions
CDK unit tests with assertions.Template are fast and require no AWS account — but they only verify the CloudFormation template you generate, not what AWS actually creates. Integration tests deploy real stacks, make real API calls, and assert on real cloud state.
The @aws-cdk/integ-tests-alpha library is CDK's official integration testing framework. It handles stack deployment, assertion execution, and cleanup — and integrates with the integ-runner CLI that AWS uses for CDK's own library testing.
The Integration Testing Stack
AWS CDK integration tests sit above snapshot tests in the testing pyramid:
Fast, no AWS ←──────────────────────────────→ Slow, real AWS
[Unit: Template assertions] → [Integration: integ-tests-alpha] → [E2E: app tests]
seconds 5–20 minutes 20+ minutesUse integration tests for:
- Verifying IAM permissions actually work
- Confirming Lambda functions can reach VPC resources
- Asserting that Step Functions state machines execute correctly
- Validating cross-service wiring (SQS → Lambda trigger, S3 → EventBridge rule)
Setup
npm install --save-dev @aws-cdk/integ-tests-alpha @aws-cdk/integ-runnerinteg-tests-alpha is in alpha — the API may change, but it's what CDK's own team uses. The integ-runner CLI runs integration test files and manages snapshot updates.
Writing Your First Integration Test
An integration test is a CDK app with one key addition: an IntegTest construct that registers stacks for deployment and runs assertions.
// integ.my-construct.ts
import * as cdk from 'aws-cdk-lib';
import { IntegTest } from '@aws-cdk/integ-tests-alpha';
import { MyLambdaStack } from '../lib/my-lambda-stack';
const app = new cdk.App();
const stack = new MyLambdaStack(app, 'IntegTestStack', {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
},
});
// Register for integ-runner
new IntegTest(app, 'IntegTest', {
testCases: [stack],
cdkCommandOptions: {
destroy: {
args: {
force: true, // Auto-destroy after test
},
},
},
});Run with integ-runner:
# Run and update snapshots
npx integ-runner --app <span class="hljs-string">"npx ts-node integ.my-construct.ts"
<span class="hljs-comment"># Run without updating snapshots (CI mode)
npx integ-runner --app <span class="hljs-string">"npx ts-node integ.my-construct.ts" --no-cleanAssertions: Calling AWS APIs After Deployment
The real power is IntegTest.assertions — it lets you call AWS APIs and assert on the responses after the stack deploys.
Invoking a Lambda Function
const integ = new IntegTest(app, 'LambdaInteg', {
testCases: [stack],
});
// Invoke the Lambda and assert on the response
const invoke = integ.assertions.invokeFunction({
functionName: stack.myFunction.functionName,
payload: JSON.stringify({ key: 'value' }),
});
invoke.expect(
ExpectedResult.objectLike({
StatusCode: 200,
Payload: '{"result":"success"}',
})
);Querying SSM Parameter Store
const getParam = integ.assertions.awsApiCall('SSM', 'getParameter', {
Name: '/my-app/config/endpoint',
});
getParam.expect(
ExpectedResult.objectLike({
Parameter: {
Value: ExpectedResult.stringLikeRegexp('https://.*\\.execute-api\\..*\\.amazonaws\\.com'),
},
})
);Checking S3 Object Existence
const getObject = integ.assertions.awsApiCall('S3', 'headObject', {
Bucket: stack.dataBucket.bucketName,
Key: 'init-marker',
});
// Just assert the call succeeds (200 response)
getObject.expect(ExpectedResult.objectLike({}));SQS Queue Assertions
// Send a message to the queue and verify processing
const sendMsg = integ.assertions.awsApiCall('SQS', 'sendMessage', {
QueueUrl: stack.inputQueue.queueUrl,
MessageBody: JSON.stringify({ orderId: '12345', action: 'process' }),
});
sendMsg.next(
integ.assertions.awsApiCall('SQS', 'receiveMessage', {
QueueUrl: stack.outputQueue.queueUrl,
WaitTimeSeconds: 10,
})
).expect(
ExpectedResult.objectLike({
Messages: [
{
Body: ExpectedResult.stringLikeRegexp('orderId.*12345'),
},
],
})
);Waiting for Async Operations
Infrastructure often involves async workflows. Use waitForAssertions to poll until a condition is met.
// Wait for Step Functions execution to complete
const startExecution = integ.assertions.awsApiCall(
'StepFunctions',
'startExecution',
{
stateMachineArn: stack.stateMachine.stateMachineArn,
input: JSON.stringify({ orderId: 'test-001' }),
}
);
const executionArn = startExecution.getAttString('executionArn');
// Poll until execution is no longer RUNNING
integ.assertions
.awsApiCall('StepFunctions', 'describeExecution', {
executionArn,
})
.waitForAssertions({
totalTimeout: cdk.Duration.minutes(5),
interval: cdk.Duration.seconds(15),
})
.expect(
ExpectedResult.objectLike({
status: 'SUCCEEDED',
})
);waitForAssertions is implemented as a custom resource that Lambda runs in a polling loop — no test runner process stays open.
Testing VPC Connectivity
Some infrastructure tests require a running compute resource inside the VPC to verify network paths. Use a Lambda function deployed inside the VPC as your "test agent":
// In your integ stack
const testFunction = new lambda.Function(scope, 'VpcTestLambda', {
vpc: stack.vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'index.handler',
code: lambda.Code.fromInline(`
const https = require('https');
exports.handler = async (event) => {
return new Promise((resolve, reject) => {
https.get(event.url, (res) => {
resolve({ statusCode: res.statusCode });
}).on('error', reject);
});
};
`),
});
// Assert VPC endpoint connectivity
const connectivityCheck = integ.assertions.invokeFunction({
functionName: testFunction.functionName,
payload: JSON.stringify({
url: `https://${stack.rdsEndpoint}:5432`,
}),
});
connectivityCheck.expect(
ExpectedResult.objectLike({ statusCode: 200 })
);Multi-Stack Integration Tests
Test cross-stack references by deploying multiple stacks in sequence:
const producerStack = new ProducerStack(app, 'ProducerIntegStack');
const consumerStack = new ConsumerStack(app, 'ConsumerIntegStack', {
topicArn: producerStack.topic.topicArn,
});
const integ = new IntegTest(app, 'CrossStackInteg', {
testCases: [producerStack, consumerStack],
// consumerStack deploys after producerStack
});
// Test the integration
const publish = integ.assertions.awsApiCall('SNS', 'publish', {
TopicArn: producerStack.topic.topicArn,
Message: JSON.stringify({ event: 'test' }),
});
// Consumer should have received the message
integ.assertions
.awsApiCall('SQS', 'receiveMessage', {
QueueUrl: consumerStack.queue.queueUrl,
WaitTimeSeconds: 20,
})
.waitForAssertions({
totalTimeout: cdk.Duration.minutes(2),
interval: cdk.Duration.seconds(10),
})
.expect(
ExpectedResult.objectLike({
Messages: ExpectedResult.arrayWith([
ExpectedResult.objectLike({
Body: ExpectedResult.stringLikeRegexp('event.*test'),
}),
]),
})
);Snapshot Management with integ-runner
integ-runner manages snapshots of your deployed stacks' CloudFormation templates. On first run, it creates snapshots. On subsequent runs, it diffs against them.
# Update snapshots after intentional changes
npx integ-runner --update-on-failed
<span class="hljs-comment"># Show diff without deploying
npx integ-runner --dry-run
<span class="hljs-comment"># Run specific test files
npx integ-runner integ.my-construct.tsCommit snapshots to git. The diff in CI surfaces unexpected infrastructure changes — even if all unit tests pass.
CI Pipeline Setup
# .github/workflows/cdk-integ.yml
name: CDK Integration Tests
on:
pull_request:
paths:
- 'lib/**'
- 'test/integ.*'
jobs:
integ-test:
runs-on: ubuntu-latest
timeout-minutes: 45
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.CDK_INTEG_TEST_ROLE_ARN }}
aws-region: us-east-1
- name: Run CDK integration tests
run: |
npx integ-runner \
--app "npx ts-node" \
--test-regex "integ\\..*\\.ts$" \
--parallel-regions us-east-1
env:
CDK_DEFAULT_ACCOUNT: ${{ secrets.AWS_ACCOUNT_ID }}
CDK_DEFAULT_REGION: us-east-1Cost Considerations
Integration tests deploy real AWS resources and incur charges. Keep costs manageable:
- Use
--destroyin CI. SetcdkCommandOptions.destroy.args.force = trueso stacks are always cleaned up. - Tag test resources. Add
Tags.of(stack).add('CostCenter', 'integration-tests')for cost allocation. - Use smallest viable sizes.
t3.micro, Lambda with 128 MB memory, single-AZ RDS. - Run in one region. Avoid multi-region integration tests unless you're specifically testing multi-region architecture.
A typical integration test for a Lambda + SQS + DynamoDB stack costs under $0.01 per run.
Comparison: integ-tests-alpha vs Alternatives
| Approach | Speed | AWS Needed | Real State Verified |
|---|---|---|---|
assertions.Template unit tests |
Fast (seconds) | No | No |
integ-tests-alpha |
Slow (5–20 min) | Yes | Yes |
| Custom Pulumi tests | Slow | Yes | Yes |
| Terratest for CDK | Slow | Yes | Yes (Go SDK) |
integ-tests-alpha is the CDK-native choice. It's the same framework the CDK team uses internally, so it tracks closely with CDK releases.
Summary
CDK integration testing with integ-tests-alpha:
- Deploys real stacks and asserts on real AWS API responses
IntegTest.assertions.awsApiCallfor arbitrary AWS SDK callswaitForAssertionsfor async workflows (Step Functions, SQS processing)integ-runnermanages deployment, snapshot tracking, and cleanup- Commit snapshots to catch unintended CloudFormation changes in CI
The pattern complements template unit tests: fast snapshot tests catch regressions in every PR, integration tests verify live behavior on merge to main.