AWS CDK Integration Testing with integ-tests-alpha: Real Deployment Assertions

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+ minutes

Use 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-runner

integ-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-clean

Assertions: 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.ts

Commit 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-1

Cost Considerations

Integration tests deploy real AWS resources and incur charges. Keep costs manageable:

  1. Use --destroy in CI. Set cdkCommandOptions.destroy.args.force = true so stacks are always cleaned up.
  2. Tag test resources. Add Tags.of(stack).add('CostCenter', 'integration-tests') for cost allocation.
  3. Use smallest viable sizes. t3.micro, Lambda with 128 MB memory, single-AZ RDS.
  4. 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.awsApiCall for arbitrary AWS SDK calls
  • waitForAssertions for async workflows (Step Functions, SQS processing)
  • integ-runner manages 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.

Read more