AWS Testing with LocalStack and TestContainers

AWS Testing with LocalStack + Testcontainers

AWS Testing with LocalStack + Testcontainers for Fast, Reliable Cloud Mocks shows how modern Java teams can run fully isolated AWS integration tests without touching real cloud infrastructure. By combining LocalStack with Testcontainers, developers can simulate services like S3, Lambda, and DynamoDB directly in their CI pipelines, achieving fast feedback loops and deterministic test execution.


Why AWS Testing is Hard in Java Projects

Testing AWS-dependent systems traditionally suffers from:

  • Dependency on real AWS accounts (cost + risk)
  • Flaky integration tests due to network latency
  • Hard-to-reproduce environments across CI/CD pipelines
  • Complex setup for mocking multiple AWS services

This is where LocalStack + Testcontainers becomes a game changer.


What is LocalStack?

LocalStack is a fully functional AWS cloud stack emulator that runs locally and provides APIs compatible with AWS services.

Key Features:

  • Emulates AWS APIs locally
  • Supports S3, Lambda, SQS, DynamoDB, SNS, and more
  • Works seamlessly with AWS SDKs

👉 Official docs: https://docs.localstack.cloud/


What is Testcontainers?

Testcontainers is a Java library that provides lightweight, disposable containers for integration testing.

Why it matters:

  • Spins up Docker containers automatically
  • Perfect for integration testing
  • Works with JUnit 4 & JUnit 5
  • Ensures isolated test environments

👉 Official site: https://testcontainers.com/


Why Combine LocalStack + Testcontainers?

Together they create a powerful testing architecture:

ToolResponsibility
LocalStackAWS service emulation
TestcontainersContainer lifecycle management

Benefits:

  • No manual Docker setup
  • Fully reproducible AWS environments
  • CI-friendly execution
  • Fast test cycles

Setting Up the Environment

Maven Dependencies

<dependencies>
<!-- AWS SDK -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>2.25.0</version>
</dependency>
<!-- Testcontainers -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.20.0</version>
<scope>test</scope> '
</dependency>
<!-- JUnit 5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
</dependencies>

Running LocalStack with Testcontainers

Java Example: Start LocalStack Container


import org.testcontainers.containers.localstack.LocalStackContainer;
import org.testcontainers.utility.DockerImageName;
import org.junit.jupiter.api.*;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.services.s3.model.*;
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class S3LocalStackTest {
static LocalStackContainer localstack =
new LocalStackContainer(DockerImageName.parse("localstack/localstack:3.0"))
.withServices(LocalStackContainer.Service.S3);
S3Client s3;
@BeforeAll
void setup() {
localstack.start();
s3 = S3Client.builder()
.endpointOverride(localstack.getEndpointOverride(LocalStackContainer.Service.S3))
.credentialsProvider(
StaticCredentialsProvider.create(
AwsBasicCredentials.create("test", "test")
)
)
.region(Region.US_EAST_1)
.build();
}
@AfterAll
void tearDown() {
localstack.stop();
}
}

Example 1: Create and Verify S3 Bucket

@Test
void shouldCreateS3Bucket() {
String bucketName = "test-bucket";
s3.createBucket(CreateBucketRequest.builder()
.bucket(bucketName)
.build());
ListBucketsResponse response = s3.listBuckets();
boolean exists = response.buckets()
.stream()
.anyMatch(b -> b.name().equals(bucketName));
Assertions.assertTrue(exists);
}

Example 2: Upload and Read Object from S3

import software.amazon.awssdk.core.sync.RequestBody;
@Test
void shouldUploadAndReadObject() {
String bucket = "demo-bucket";
String key = "file.txt";
s3.createBucket(CreateBucketRequest.builder()
.bucket(bucket)
.build());
s3.putObject(
PutObjectRequest.builder()
.bucket(bucket)
.key(key)
.build(),
RequestBody.fromString("Hello AWS LocalStack")
);
String content = s3.getObjectAsBytes(
GetObjectRequest.builder()
.bucket(bucket)
.key(key)
.build()
).asUtf8String();
Assertions.assertEquals("Hello AWS LocalStack", content);
}

Example 3: DynamoDB Test with LocalStack

import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
@Test
void shouldCreateDynamoTable() {
DynamoDbClient dynamo = DynamoDbClient.builder()
.endpointOverride(localstack.getEndpointOverride(LocalStackContainer.Service.DYNAMODB))
.credentialsProvider(
StaticCredentialsProvider.create(
AwsBasicCredentials.create("test", "test")
)
)
.region(Region.US_EAST_1)
.build();
dynamo.createTable(CreateTableRequest.builder()
.tableName("Users")
.keySchema(KeySchemaElement.builder()
.attributeName("id")
.keyType(KeyType.HASH)
.build())
.attributeDefinitions(AttributeDefinition.builder()
.attributeName("id")
.attributeType(ScalarAttributeType.S)
.build())
.provisionedThroughput(ProvisionedThroughput.builder()
.readCapacityUnits(1L)
.writeCapacityUnits(1L)
.build())
.build());
Assertions.assertTrue(
dynamo.listTables().tableNames().contains("Users")
);
}

Best Practices for AWS Testing with LocalStack

  • Use separate containers per test suite
  • Always override AWS endpoint in SDK
  • Keep credentials dummy (test/test)
  • Prefer Testcontainers lifecycle hooks (@BeforeAll, @AfterAll)
  • Avoid mixing real AWS calls in integration tests

CI/CD Integration

One of the biggest advantages of this setup is CI compatibility.

Popular CI systems like:

  • GitHub Actions
  • GitLab CI
  • Jenkins

can run LocalStack + Testcontainers seamlessly because everything runs inside Docker.


Common Pitfalls

  • Not exposing correct LocalStack service ports
  • Forgetting endpointOverride in AWS SDK
  • Reusing containers across tests (causes state leakage)
  • Mixing unit tests and integration tests

Useful Links

Related Resources

AWS Testing Page

Testing AWS locally using Moto, Boto3 & Robot Framework


Conclusion

Combining LocalStack and Testcontainers creates a powerful and production-grade testing strategy for AWS-based Java applications. It eliminates cloud dependency, improves test speed, and ensures deterministic behavior across environments—making it one of the most effective approaches for modern cloud-native testing pipelines.


Discover more from Rotebit

Subscribe to get the latest posts sent to your email.

1 Comment

Leave a Reply