AWS Builder Center
Test Your Cloud Like You Mean It: Integrating Testcontainers and Localstack

Test Your Cloud Like You Mean It: Integrating Testcontainers and Localstack

This article explains how to use Testcontainers and Localstack to test AWS services locally. It shows how to create Lambda functions triggered by SQS messages in a containerized environment. The setup helps developers test cloud code quickly without deploying to real AWS.

You know the story: all your unit tests are green, CI is happy, the pipeline is a sea of checkmarks. Then you hit a real cloud environment, and everything falls apart.
Suddenly, you discover the things your mocks never told you:
  • RDS parameter quirks
  • SQS/SNS throughput limits
  • Lambda and API Gateway behavior that doesn’t quite match your local stubs
That gap between “mocked reality” and “actual AWS” is exactly why I’ve become obsessed with integration tests against LocalStack.

Why Unit Tests Aren’t Enough

Unit tests are great at telling me if a single function behaves as I expect in isolation. But my real problems rarely live inside a single function. They live:
  • In IAM permissions
  • In event payload shapes
  • In how Lambda, SQS, SNS, and API Gateway talk to each other
  • In misconfigured endpoints and timeouts
Mocks are, by definition, my own guess of how AWS behaves. As my systems grow more distributed, that guess becomes more dangerous.
I don’t just want to know that my handler code works. I want to see a real Lambda, triggered by a real event, talking to a real queue, and logging into a real CloudWatch instance – even if all of that is running locally in containers.

Enter Testcontainers + LocalStack

Testcontainers lets me programmatically spin up real infrastructure components in Docker as part of my test suite: databases, message brokers, and, via LocalStack, AWS APIs themselves.
Different Phases how Testcontainers Work
Different Phases how Testcontainers Work
With the LocalStack Testcontainers module, my tests can:
  • Start a LocalStack container on a random port for true isolation
  • Configure AWS SDK / CDK to talk to that specific endpoint
  • Run against real AWS APIs implemented by LocalStack
  • Tear everything down automatically at the end of the run
That means I can run the same integration suite both on my machine and in CI, without port conflicts or shared, flaky state.

What I Actually Test

A realistic example I like is a simple API Gateway flow:
  1. An API Gateway HTTP method
  2. A Lambda function gets triggered
With CDK, I define a stack that wires up:
  • An API Gateway
  • A Lambda function (Node.js/TypeScript)
  • The event is sent to the Lambda by a specific endpoint
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { afterAll, beforeAll } from "bun:test";
import {
LocalstackContainer,
type StartedLocalStackContainer,
} from "@testcontainers/localstack";
import { $ } from "bun";

const foo = function ApiGatewayLambdaStack() {}

describe("integ:apigateway-lambda", () => {
const ls = createLocalStackSetup({
appFile: import.meta.path,
stack: foo,
outputsFile: "/tmp/cdk-outputs.json",
});

it("should return 200 from GET /lambda via REST API", async () => {
const outputs = await Bun.file("/tmp/cdk-outputs.json").json();
const stackOutputs = Object.values(outputs)[0] as Record<string, string>;
const cfnUrl = Object.values(stackOutputs).find((v) =>
v.includes("execute-api"),
);
const apiId = new URL(cfnUrl).hostname.split(".")[0];
const apiUrl = `${ls.endpoint}/restapis/${apiId}/prod/_user_request_`;
const res = await fetch(`${apiUrl}/lambda`);

expect(res.status).toBe(200);
});
});
In my integration test, I:
  1. Start LocalStack via Testcontainers (createLocalStackSetupis a wrapper)
  2. Bootstrap and deploy the CDK stack against LocalStack (using cdk local / a local entrypoint)
  3. Use the AWS CLI (pointed at LocalStack) to:
    • Localstack dumps a cdk-outputs.json
      • It contains the CDK synth CloudFormation file
    • Create a Lambda File
  4. Waits for the Lambda to be triggered
  5. Check status message
If that passes, I’ve tested:
  • CDK synthesis and deployment
  • Event source mapping wiring
  • Lambda handler code
  • Logging/observability
  • AWS CLI and SDK configuration
All without touching real AWS.

LocalStack in the Loop

LocalStack is the backbone that makes these tests valuable.
Because LocalStack runs real implementations of AWS APIs inside Docker, I can surface the kind of issues I’d otherwise only see in staging:
  • Wrong endpoint URLs or regions
  • Misconfigured credentials
  • APIs that behave slightly differently from my mocks
  • Resource naming or ARNs that don’t line up with what AWS expects
And since Testcontainers creates and cleans up the LocalStack container for each test suite, I don’t have to manage lifecycle or worry about leaking containers or ports.

Where Pawl Comes In

At some point, I realized I was repeating the same patterns over and over again in CDK projects: wrapping Lambda constructs with best practices, enforcing retry policies on queues, centralizing naming conventions, and so on. That’s how pawl  was born.
pawl stands for “Professional AWS Library” – and yes, I picture him as a little dog that follows me around and learns new tricks.
pawl- Proficient AWS Library
pawl- Proficient AWS Library
pawl is a CDK construct library (and more) that:
  • Encodes best practices into reusable constructs
  • Improves DX with TypeScript types and autocompletion
  • Enforces conventions like:
    • Lambda runtime (e.g. latest Node.js, ARM64)
    • Minimal bundle sizes and tree-shaking
    • Required properties such as retries for SQS consumers
    • Company-wide naming schemes and dead-letter queues
So instead of rewriting boilerplate every time I start a new stack, I drop in Pawl constructs and get “sane defaults” out of the box. More in upcoming blog posts.
The beautiful part is how pawl, LocalStack, and Testcontainers fit together:
  • Pawl defines opinionated, high–level AWS constructs
  • CDK + LocalStack deploy those constructs into a local cloud
  • Testcontainers spins that local cloud up and down as part of my tests
That combination lets me iterate on infrastructure, code, and tests with fast feedback and high confidence – before I ever touch a real AWS account.
pawl the Dog rides on the Localstack Rocket
pawl, Testcontainers and LocalStack - Dream Team
If you’re tired of the surprise you get between “all green in CI” and “why is staging on fire?”, integrating LocalStack + Testcontainers into your workflow – and wrapping it with something like Pawl – is one of the most effective upgrades you can make to your testing strategy.
Disclaimer: The article is generated with the help of an LLM.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article