docs: prune specialized Testcontainers integration guides

The Testcontainers collection includes nine specialized Java integration
tutorials with modest readership and overlapping framework setup.

Remove these guides and their links from the Testcontainers overview,
retaining eleven guides covering language introductions, general
techniques, representative web frameworks, and Testcontainers Cloud.
This commit is contained in:
David Karlsson committed 2026-10-08 13:50:14 +00:00
1 parent 7a1b2c0627
commit b24d663344
10 files changed
-4979

No files matched your search

@@ -1,450 +0,0 @@
---
title: Testing AWS service integrations using LocalStack
linkTitle: AWS LocalStack
description: Learn how to test Spring Cloud AWS applications using LocalStack and Testcontainers.
keywords: testcontainers, java, spring boot, testing, aws, localstack, s3, sqs, spring cloud aws
summary: |
Learn how to create a Spring Boot application with Spring Cloud AWS,
then test S3 and SQS integrations using Testcontainers and LocalStack.
aliases:
- /guides/testcontainers-java-aws-localstack/create-project/
- /guides/testcontainers-java-aws-localstack/run-tests/
- /guides/testcontainers-java-aws-localstack/write-tests/
params:
tags: [testing]
time: 25 minutes
---
<!-- Source: https://github.com/testcontainers/tc-guide-testing-aws-service-integrations-using-localstack -->
In this guide, you will learn how to:
- Create a Spring Boot application with Spring Cloud AWS integration
- Use AWS S3 and SQS services
- Test the application using Testcontainers and LocalStack
## Prerequisites
- Java 17+
- Maven or Gradle
- A Docker environment supported by Testcontainers
> [!NOTE]
> If you're new to Testcontainers, visit the
> [Testcontainers overview](https://testcontainers.com/getting-started/) to learn more about
> Testcontainers and the benefits of using it.
## Create the Spring Boot project
### Set up the project
Create a Spring Boot project from [Spring Initializr](https://start.spring.io)
by selecting the **Testcontainers** starter. Spring Cloud AWS starters are not
available on Spring Initializr, so you need to add them manually.
Alternatively, clone the
[guide repository](https://github.com/testcontainers/tc-guide-testing-aws-service-integrations-using-localstack).
Add the Spring Cloud AWS BOM to your dependency management and add the S3, SQS
starters as dependencies. Testcontainers provides a
[LocalStack module](https://testcontainers.com/modules/localstack/) for testing
AWS service integrations. You also need
[Awaitility](http://www.awaitility.org/) for testing asynchronous SQS
processing.
The key dependencies in `pom.xml` are:
```xml
<properties>
<java.version>17</java.version>
<testcontainers.version>2.0.4</testcontainers.version>
<awspring.version>3.0.3</awspring.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.awspring.cloud</groupId>
<artifactId>spring-cloud-aws-starter-s3</artifactId>
</dependency>
<dependency>
<groupId>io.awspring.cloud</groupId>
<artifactId>spring-cloud-aws-starter-sqs</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-localstack</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.awspring.cloud</groupId>
<artifactId>spring-cloud-aws-dependencies</artifactId>
<version>${awspring.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
```
### Create the configuration properties
To make the SQS queue and S3 bucket names configurable, create an
`ApplicationProperties` record:
```java
package com.testcontainers.demo;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "app")
public record ApplicationProperties(String queue, String bucket) {}
```
Then add `@ConfigurationPropertiesScan` to the main application class so that
Spring automatically scans for `@ConfigurationProperties`-annotated classes and
registers them as beans:
```java
package com.testcontainers.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
@SpringBootApplication
@ConfigurationPropertiesScan
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
```
### Implement StorageService for S3
Spring Cloud AWS provides higher-level abstractions like `S3Template` with
convenience methods for uploading and downloading files. Create a
`StorageService` class:
```java
package com.testcontainers.demo;
import io.awspring.cloud.s3.S3Template;
import java.io.IOException;
import java.io.InputStream;
import org.springframework.stereotype.Service;
@Service
public class StorageService {
private final S3Template s3Template;
public StorageService(S3Template s3Template) {
this.s3Template = s3Template;
}
public void upload(String bucketName, String key, InputStream stream) {
this.s3Template.upload(bucketName, key, stream);
}
public InputStream download(String bucketName, String key)
throws IOException {
return this.s3Template.download(bucketName, key).getInputStream();
}
public String downloadAsString(String bucketName, String key)
throws IOException {
try (InputStream is = this.download(bucketName, key)) {
return new String(is.readAllBytes());
}
}
}
```
### Create the SQS message model
Create a `Message` record that represents the payload you send to the SQS
queue:
```java
package com.testcontainers.demo;
import java.util.UUID;
public record Message(UUID uuid, String content) {}
```
### Implement the message sender
Create `MessageSender`, which uses `SqsTemplate` to publish messages:
```java
package com.testcontainers.demo;
import io.awspring.cloud.sqs.operations.SqsTemplate;
import org.springframework.stereotype.Service;
@Service
public class MessageSender {
private final SqsTemplate sqsTemplate;
public MessageSender(SqsTemplate sqsTemplate) {
this.sqsTemplate = sqsTemplate;
}
public void publish(String queueName, Message message) {
sqsTemplate.send(to -> to.queue(queueName).payload(message));
}
}
```
### Implement the message listener
Create `MessageListener` with a handler method annotated with `@SqsListener`.
When a message arrives, the listener uploads the content to an S3 bucket using
the message UUID as the key:
```java
package com.testcontainers.demo;
import io.awspring.cloud.sqs.annotation.SqsListener;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import org.springframework.stereotype.Service;
@Service
public class MessageListener {
private final StorageService storageService;
private final ApplicationProperties properties;
public MessageListener(
StorageService storageService,
ApplicationProperties properties
) {
this.storageService = storageService;
this.properties = properties;
}
@SqsListener(queueNames = { "${app.queue}" })
public void handle(Message message) {
String bucketName = this.properties.bucket();
String key = message.uuid().toString();
ByteArrayInputStream is = new ByteArrayInputStream(
message.content().getBytes(StandardCharsets.UTF_8)
);
this.storageService.upload(bucketName, key, is);
}
}
```
The `${app.queue}` expression reads the queue name from application
configuration instead of hard-coding it.
## Write tests with Testcontainers
To test the application, you need a running LocalStack instance that emulates
the AWS S3 and SQS services. Testcontainers spins up LocalStack in a Docker
container and `@DynamicPropertySource` connects it to Spring Cloud AWS.
### Configure the test container
You can start a LocalStack container and configure the Spring Cloud AWS
properties to talk to it instead of actual AWS services. The properties you
need to set are:
```properties
spring.cloud.aws.s3.endpoint=http://localhost:4566
spring.cloud.aws.sqs.endpoint=http://localhost:4566
spring.cloud.aws.credentials.access-key=noop
spring.cloud.aws.credentials.secret-key=noop
spring.cloud.aws.region.static=us-east-1
```
For testing, use an ephemeral container that starts on a random available port
so that you can run multiple builds in CI in parallel without port conflicts.
### Write the test
Create `MessageListenerTest.java`:
```java
package com.testcontainers.demo;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.testcontainers.containers.localstack.LocalStackContainer.Service.S3;
import static org.testcontainers.containers.localstack.LocalStackContainer.Service.SQS;
import java.io.IOException;
import java.time.Duration;
import java.util.UUID;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.localstack.LocalStackContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
@SpringBootTest
@Testcontainers
class MessageListenerTest {
@Container
static LocalStackContainer localStack = new LocalStackContainer(
DockerImageName.parse("localstack/localstack:3.0")
);
static final String BUCKET_NAME = UUID.randomUUID().toString();
static final String QUEUE_NAME = UUID.randomUUID().toString();
@DynamicPropertySource
static void overrideProperties(DynamicPropertyRegistry registry) {
registry.add("app.bucket", () -> BUCKET_NAME);
registry.add("app.queue", () -> QUEUE_NAME);
registry.add(
"spring.cloud.aws.region.static",
() -> localStack.getRegion()
);
registry.add(
"spring.cloud.aws.credentials.access-key",
() -> localStack.getAccessKey()
);
registry.add(
"spring.cloud.aws.credentials.secret-key",
() -> localStack.getSecretKey()
);
registry.add(
"spring.cloud.aws.s3.endpoint",
() -> localStack.getEndpointOverride(S3).toString()
);
registry.add(
"spring.cloud.aws.sqs.endpoint",
() -> localStack.getEndpointOverride(SQS).toString()
);
}
@BeforeAll
static void beforeAll() throws IOException, InterruptedException {
localStack.execInContainer("awslocal", "s3", "mb", "s3://" + BUCKET_NAME);
localStack.execInContainer(
"awslocal",
"sqs",
"create-queue",
"--queue-name",
QUEUE_NAME
);
}
@Autowired
StorageService storageService;
@Autowired
MessageSender publisher;
@Autowired
ApplicationProperties properties;
@Test
void shouldHandleMessageSuccessfully() {
Message message = new Message(UUID.randomUUID(), "Hello World");
publisher.publish(properties.queue(), message);
await()
.pollInterval(Duration.ofSeconds(2))
.atMost(Duration.ofSeconds(10))
.ignoreExceptions()
.untilAsserted(() -> {
String msg = storageService.downloadAsString(
properties.bucket(),
message.uuid().toString()
);
assertThat(msg).isEqualTo("Hello World");
});
}
}
```
Here's what the test does:
- `@SpringBootTest` starts the full Spring application context.
- The Testcontainers JUnit 5 annotations `@Testcontainers` and `@Container`
manage the lifecycle of a `LocalStackContainer` instance.
- `@DynamicPropertySource` obtains the dynamic S3 and SQS endpoint URLs,
region, access key, and secret key from the container, and registers them as
Spring Cloud AWS configuration properties.
- `@BeforeAll` creates the required SQS queue and S3 bucket using the
`awslocal` CLI tool that comes pre-installed in the LocalStack Docker image.
The `localStack.execInContainer()` API runs commands inside the container.
- `shouldHandleMessageSuccessfully()` publishes a `Message` to the SQS queue.
The listener receives the message and stores its content in the S3 bucket
with the UUID as the key. Awaitility waits up to 10 seconds for the expected
content to appear in the bucket.
## Run tests and next steps
### Run the tests
```console
$ ./mvnw test
```
Or with Gradle:
```console
$ ./gradlew test
```
You should see the LocalStack Docker container start and the test pass. After
the tests finish, the container stops and is removed automatically.
### Summary
LocalStack lets you develop and test AWS-based applications locally.
The Testcontainers LocalStack module makes it straightforward to write
integration tests by using ephemeral LocalStack containers that start on random
ports with no external setup required.
To learn more about Testcontainers, visit the
[Testcontainers overview](https://testcontainers.com/getting-started/).
### Further reading
- [Testcontainers LocalStack module](https://java.testcontainers.org/modules/localstack/)
- [Getting started with Testcontainers for Java](https://java.testcontainers.org/quickstart/junit_5_quickstart/)
- [Spring Cloud AWS documentation](https://docs.awspring.io/spring-cloud-aws/docs/3.0.3/reference/html/index.html)
@@ -1,610 +0,0 @@
---
title: Working with jOOQ and Flyway using Testcontainers
linkTitle: jOOQ and Flyway
description: Learn how to generate jOOQ code from a database using Testcontainers and Flyway, then test your persistence layer.
keywords: testcontainers, java, testing, jooq, flyway, postgresql, spring boot, code generation
summary: |
Generate typesafe jOOQ code from a real PostgreSQL database managed by
Flyway migrations, then test repositories using Testcontainers.
aliases:
- /guides/testcontainers-java-jooq-flyway/create-project/
- /guides/testcontainers-java-jooq-flyway/run-tests/
- /guides/testcontainers-java-jooq-flyway/write-tests/
params:
tags: [testing]
time: 25 minutes
---
<!-- Source: https://github.com/testcontainers/tc-guide-working-with-jooq-flyway-using-testcontainers -->
In this guide, you will learn how to:
- Create a Spring Boot application with jOOQ support
- Generate jOOQ code using Testcontainers, Flyway, and a Maven plugin
- Implement basic database operations using jOOQ
- Load complex object graphs using jOOQ's MULTISET feature
- Test the jOOQ persistence layer using Testcontainers
## Prerequisites
- Java 17+
- Maven
- A Docker environment supported by Testcontainers
> [!NOTE]
> If you're new to Testcontainers, visit the
> [Testcontainers overview](https://testcontainers.com/getting-started/) to learn more about
> Testcontainers and the benefits of using it.
## Create the Spring Boot project
### Set up the project
Create a Spring Boot project from [Spring Initializr](https://start.spring.io)
by selecting Maven as the build tool and adding the **JOOQ Access Layer**,
**Flyway Migration**, **Spring Boot DevTools**, **PostgreSQL Driver**, and
**Testcontainers** starters.
Alternatively, clone the
[guide repository](https://github.com/testcontainers/tc-guide-working-with-jooq-flyway-using-testcontainers).
jOOQ (jOOQ Object Oriented Querying) provides a fluent API for building
typesafe SQL queries. To get the full benefit of its typesafe DSL, you need
to generate Java code from your database tables, views, and other objects.
> [!TIP]
> To learn more about how the jOOQ code generator helps, read
> [Why You Should Use jOOQ With Code Generation](https://blog.jooq.org/why-you-should-use-jooq-with-code-generation/).
The typical process for building and testing the application with jOOQ code
generation is:
1. Create a database instance using Testcontainers.
2. Apply Flyway database migrations.
3. Run the jOOQ code generator to produce Java code from the database objects.
4. Run integration tests.
The
[testcontainers-jooq-codegen-maven-plugin](https://github.com/testcontainers/testcontainers-jooq-codegen-maven-plugin)
automates this as part of the Maven build.
### Create Flyway migration scripts
The sample application has `users`, `posts`, and `comments` tables. Create
the first migration script following the Flyway naming convention.
Create `src/main/resources/db/migration/V1__create_tables.sql`:
```sql
create table users
(
id bigserial not null,
name varchar not null,
email varchar not null,
created_at timestamp,
updated_at timestamp,
primary key (id),
constraint user_email_unique unique (email)
);
create table posts
(
id bigserial not null,
title varchar not null,
content varchar not null,
created_by bigint references users (id) not null,
created_at timestamp,
updated_at timestamp,
primary key (id)
);
create table comments
(
id bigserial not null,
name varchar not null,
content varchar not null,
post_id bigint references posts (id) not null,
created_at timestamp,
updated_at timestamp,
primary key (id)
);
ALTER SEQUENCE users_id_seq RESTART WITH 101;
ALTER SEQUENCE posts_id_seq RESTART WITH 101;
ALTER SEQUENCE comments_id_seq RESTART WITH 101;
```
The sequence values restart at 101 so that you can insert sample data with
explicit primary key values for testing.
### Configure jOOQ code generation
Add the `testcontainers-jooq-codegen-maven-plugin` to `pom.xml`:
```xml
<properties>
<testcontainers.version>2.0.4</testcontainers.version>
<testcontainers-jooq-codegen-maven-plugin.version>0.0.4</testcontainers-jooq-codegen-maven-plugin.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-jooq-codegen-maven-plugin</artifactId>
<version>${testcontainers-jooq-codegen-maven-plugin.version}</version>
<dependencies>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-postgresql</artifactId>
<version>${testcontainers.version}</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>${postgresql.version}</version>
</dependency>
</dependencies>
<executions>
<execution>
<id>generate-jooq-sources</id>
<goals>
<goal>generate</goal>
</goals>
<phase>generate-sources</phase>
<configuration>
<database>
<type>POSTGRES</type>
<containerImage>postgres:16-alpine</containerImage>
</database>
<flyway>
<locations>
filesystem:src/main/resources/db/migration
</locations>
</flyway>
<jooq>
<generator>
<database>
<includes>.*</includes>
<excludes>flyway_schema_history</excludes>
<inputSchema>public</inputSchema>
</database>
<target>
<packageName>com.testcontainers.demo.jooq</packageName>
<directory>target/generated-sources/jooq</directory>
</target>
</generator>
</jooq>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
```
Here's what the plugin configuration does:
- The `<configuration>/<database>` section sets the database type to
`POSTGRES` and the Docker image to `postgres:16-alpine`.
- The `<configuration>/<flyway>` section points to the Flyway migration
scripts.
- The `<configuration>/<jooq>` section configures the package name and
output directory for the generated code. You can use any configuration
option that the official `jooq-code-generator` plugin supports.
When you run `./mvnw clean package`, the plugin uses Testcontainers to
spin up a PostgreSQL container, applies the Flyway migrations, and generates
Java code under `target/generated-sources/jooq`.
### Create model classes
Create model classes to represent the data structures for various use cases.
These records hold a subset of column values from the tables.
`User.java`:
```java
package com.testcontainers.demo.domain;
public record User(Long id, String name, String email) {}
```
`Post.java`:
```java
package com.testcontainers.demo.domain;
import java.time.LocalDateTime;
import java.util.List;
public record Post(
Long id,
String title,
String content,
User createdBy,
List<Comment> comments,
LocalDateTime createdAt,
LocalDateTime updatedAt
) {}
```
`Comment.java`:
```java
package com.testcontainers.demo.domain;
import java.time.LocalDateTime;
public record Comment(
Long id,
String name,
String content,
LocalDateTime createdAt,
LocalDateTime updatedAt
) {}
```
### Implement repositories using jOOQ
Create `UserRepository.java` with methods to create a user and look up a user
by email:
```java
package com.testcontainers.demo.domain;
import static com.testcontainers.demo.jooq.tables.Users.USERS;
import static org.jooq.Records.mapping;
import java.time.LocalDateTime;
import java.util.Optional;
import org.jooq.DSLContext;
import org.springframework.stereotype.Repository;
@Repository
class UserRepository {
private final DSLContext dsl;
UserRepository(DSLContext dsl) {
this.dsl = dsl;
}
public User createUser(User user) {
return this.dsl.insertInto(USERS)
.set(USERS.NAME, user.name())
.set(USERS.EMAIL, user.email())
.set(USERS.CREATED_AT, LocalDateTime.now())
.returningResult(USERS.ID, USERS.NAME, USERS.EMAIL)
.fetchOne(mapping(User::new));
}
public Optional<User> getUserByEmail(String email) {
return this.dsl.select(USERS.ID, USERS.NAME, USERS.EMAIL)
.from(USERS)
.where(USERS.EMAIL.equalIgnoreCase(email))
.fetchOptional(mapping(User::new));
}
}
```
The jOOQ DSL looks similar to SQL but written in Java. Because the code is
generated from the database schema, it stays in sync with the database
structure and provides type safety. For example,
`where(USERS.EMAIL.equalIgnoreCase(email))` expects a `String` value. If you
pass a non-string value like `123`, you get a compiler error.
### Fetch complex object graphs
jOOQ shines when it comes to complex queries. The database has a many-to-one
relationship from `Post` to `User` and a one-to-many relationship from `Post`
to `Comment`.
Create `PostRepository.java` to load a `Post` with its creator and comments
using a single query with jOOQ's MULTISET feature:
```java
package com.testcontainers.demo.domain;
import static com.testcontainers.demo.jooq.Tables.COMMENTS;
import static com.testcontainers.demo.jooq.tables.Posts.POSTS;
import static org.jooq.Records.mapping;
import static org.jooq.impl.DSL.multiset;
import static org.jooq.impl.DSL.row;
import static org.jooq.impl.DSL.select;
import java.util.Optional;
import org.jooq.DSLContext;
import org.springframework.stereotype.Repository;
@Repository
class PostRepository {
private final DSLContext dsl;
PostRepository(DSLContext dsl) {
this.dsl = dsl;
}
public Optional<Post> getPostById(Long id) {
return this.dsl.select(
POSTS.ID,
POSTS.TITLE,
POSTS.CONTENT,
row(POSTS.users().ID, POSTS.users().NAME, POSTS.users().EMAIL)
.mapping(User::new)
.as("createdBy"),
multiset(
select(
COMMENTS.ID,
COMMENTS.NAME,
COMMENTS.CONTENT,
COMMENTS.CREATED_AT,
COMMENTS.UPDATED_AT
)
.from(COMMENTS)
.where(POSTS.ID.eq(COMMENTS.POST_ID))
)
.as("comments")
.convertFrom(r -> r.map(mapping(Comment::new))),
POSTS.CREATED_AT,
POSTS.UPDATED_AT
)
.from(POSTS)
.where(POSTS.ID.eq(id))
.fetchOptional(mapping(Post::new));
}
}
```
This uses jOOQ's
[nested records](https://www.jooq.org/doc/latest/manual/sql-building/column-expressions/nested-records/)
for the many-to-one `Post`-to-`User` association and
[MULTISET](https://www.jooq.org/doc/latest/manual/sql-building/column-expressions/multiset-value-constructor/)
for the one-to-many `Post`-to-`Comment` association.
## Write tests with Testcontainers
Before writing the tests, create an SQL script to seed test data at
`src/test/resources/test-data.sql`:
```sql
DELETE FROM comments;
DELETE FROM posts;
DELETE FROM users;
INSERT INTO users(id, name, email) VALUES
(1, 'Siva', 'siva@gmail.com'),
(2, 'Oleg', 'oleg@gmail.com');
INSERT INTO posts(id, title, content, created_by, created_at) VALUES
(1, 'Post 1 Title', 'Post 1 content', 1, CURRENT_TIMESTAMP),
(2, 'Post 2 Title', 'Post 2 content', 2, CURRENT_TIMESTAMP);
INSERT INTO comments(id, name, content, post_id, created_at) VALUES
(1, 'Ron', 'Comment 1', 1, CURRENT_TIMESTAMP),
(2, 'James', 'Comment 2', 1, CURRENT_TIMESTAMP),
(3, 'Robert', 'Comment 3', 2, CURRENT_TIMESTAMP);
```
### Test with the @JooqTest slice
The `@JooqTest` annotation loads only the persistence layer components and
auto-configures jOOQ's `DSLContext`. Use the Testcontainers special JDBC URL
to start a Postgres container.
Create `UserRepositoryJooqTest.java`:
```java
package com.testcontainers.demo.domain;
import static org.assertj.core.api.Assertions.assertThat;
import org.jooq.DSLContext;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jooq.JooqTest;
import org.springframework.test.context.jdbc.Sql;
@JooqTest(
properties = {
"spring.test.database.replace=none",
"spring.datasource.url=jdbc:tc:postgresql:16-alpine:///db",
}
)
@Sql("/test-data.sql")
class UserRepositoryJooqTest {
@Autowired
DSLContext dsl;
UserRepository repository;
@BeforeEach
void setUp() {
this.repository = new UserRepository(dsl);
}
@Test
void shouldCreateUserSuccessfully() {
User user = new User(null, "John", "john@gmail.com");
User savedUser = repository.createUser(user);
assertThat(savedUser.id()).isNotNull();
assertThat(savedUser.name()).isEqualTo("John");
assertThat(savedUser.email()).isEqualTo("john@gmail.com");
}
@Test
void shouldGetUserByEmail() {
User user = repository.getUserByEmail("siva@gmail.com").orElseThrow();
assertThat(user.id()).isEqualTo(1L);
assertThat(user.name()).isEqualTo("Siva");
assertThat(user.email()).isEqualTo("siva@gmail.com");
}
}
```
Here's what the test does:
- `@JooqTest` loads only the persistence layer and auto-configures
`DSLContext`.
- The Testcontainers special JDBC URL
(`jdbc:tc:postgresql:16-alpine:///db`) starts a PostgreSQL container
automatically.
- Because `flyway-core` is on the classpath, Spring Boot runs the Flyway
migrations from `src/main/resources/db/migration` on startup.
- `@Sql("/test-data.sql")` loads the test data before each test.
- The `UserRepository` is instantiated manually with the injected
`DSLContext`.
### Integration test with @SpringBootTest
For a full integration test, use `@SpringBootTest` with the Testcontainers
`@ServiceConnection` support introduced in Spring Boot 3.1.
Create `UserRepositoryTest.java`:
```java
package com.testcontainers.demo.domain;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.test.context.jdbc.Sql;
import org.testcontainers.postgresql.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@SpringBootTest
@Sql("/test-data.sql")
@Testcontainers
class UserRepositoryTest {
@Container
@ServiceConnection
static PostgreSQLContainer postgres = new PostgreSQLContainer(
"postgres:16-alpine"
);
@Autowired
UserRepository repository;
@Test
void shouldCreateUserSuccessfully() {
User user = new User(null, "John", "john@gmail.com");
User savedUser = repository.createUser(user);
assertThat(savedUser.id()).isNotNull();
assertThat(savedUser.name()).isEqualTo("John");
assertThat(savedUser.email()).isEqualTo("john@gmail.com");
}
@Test
void shouldGetUserByEmail() {
User user = repository.getUserByEmail("siva@gmail.com").orElseThrow();
assertThat(user.id()).isEqualTo(1L);
assertThat(user.name()).isEqualTo("Siva");
assertThat(user.email()).isEqualTo("siva@gmail.com");
}
}
```
Here's what the test does:
- `@SpringBootTest` loads the entire application context, so
`UserRepository` is injected directly.
- `@Testcontainers` and `@Container` manage the PostgreSQL container
lifecycle.
- `@ServiceConnection` auto-configures the datasource properties from the
running container, replacing the need for `@DynamicPropertySource`.
- `@Sql("/test-data.sql")` initializes the test data.
### Test PostRepository
Test the `PostRepository` that fetches complex object graphs using the
Testcontainers special JDBC URL.
Create `PostRepositoryTest.java`:
```java
package com.testcontainers.demo.domain;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.jdbc.Sql;
@SpringBootTest(
properties = {
"spring.test.database.replace=none",
"spring.datasource.url=jdbc:tc:postgresql:16-alpine:///db",
}
)
@Sql("/test-data.sql")
class PostRepositoryTest {
@Autowired
PostRepository repository;
@Test
void shouldGetPostById() {
Post post = repository.getPostById(1L).orElseThrow();
assertThat(post.id()).isEqualTo(1L);
assertThat(post.title()).isEqualTo("Post 1 Title");
assertThat(post.content()).isEqualTo("Post 1 content");
assertThat(post.createdBy().id()).isEqualTo(1L);
assertThat(post.createdBy().name()).isEqualTo("Siva");
assertThat(post.createdBy().email()).isEqualTo("siva@gmail.com");
assertThat(post.comments()).hasSize(2);
}
}
```
This test verifies that `getPostById` loads the post along with its creator
and comments in a single query using jOOQ's MULTISET feature.
## Run tests and next steps
### Run the tests
```console
$ ./mvnw test
```
You should see the PostgreSQL Docker container start, jOOQ code generation
complete, and all tests pass. After the tests finish, the container stops and
is removed automatically.
### Summary
The Testcontainers library helps you generate Java code from the database
using the jOOQ code generator and test your persistence layer against the
same type of database (PostgreSQL) that you use in production, instead of
mocks or in-memory databases.
Because the code is always generated from the database's current state, you
can be confident that your code stays in sync with database changes. You're
free to refactor and still verify that the application works as expected.
To learn more about Testcontainers, visit the
[Testcontainers overview](https://testcontainers.com/getting-started/).
### Further reading
- [jOOQ documentation](https://www.jooq.org/)
- [jOOQ code generation](https://www.jooq.org/doc/latest/manual/code-generation/)
- [Spring Boot Testcontainers support](https://docs.spring.io/spring-boot/reference/testing/testcontainers.html)
- [Replace H2 with a real database for testing](/guides/testcontainers-java-replace-h2/)
@@ -1,605 +0,0 @@
---
title: Securing Spring Boot microservice using Keycloak and Testcontainers
linkTitle: Keycloak with Spring Boot
description: Learn how to secure a Spring Boot microservice using Keycloak and test it with the Testcontainers Keycloak module.
keywords: testcontainers, java, spring boot, testing, keycloak, security, oauth2, jwt
summary: |
Learn how to create an OAuth 2.0 Resource Server using Spring Boot, secure API
endpoints with Keycloak, and test the application using the Testcontainers Keycloak module.
aliases:
- /guides/testcontainers-java-keycloak-spring-boot/create-project/
- /guides/testcontainers-java-keycloak-spring-boot/run-tests/
- /guides/testcontainers-java-keycloak-spring-boot/write-tests/
params:
tags: [testing]
time: 30 minutes
---
<!-- Source: https://github.com/testcontainers/tc-guide-securing-spring-boot-microservice-using-keycloak-and-testcontainers -->
In this guide, you'll learn how to:
- Create an OAuth 2.0 Resource Server using Spring Boot
- Secure API endpoints using Keycloak
- Test the APIs using the Testcontainers Keycloak module
- Run the application locally using the Testcontainers Keycloak module
## Prerequisites
- Java 17+
- Maven or Gradle
- A Docker environment supported by Testcontainers
> [!NOTE]
> If you're new to Testcontainers, visit the
> [Testcontainers overview](https://testcontainers.com/getting-started/) to learn more about
> Testcontainers and the benefits of using it.
## Create the Spring Boot project
### Set up the project
Create a Spring Boot project from [Spring Initializr](https://start.spring.io)
by selecting the **Spring Web**, **Validation**, **JDBC API**,
**PostgreSQL Driver**, **Spring Security**, **OAuth2 Resource Server**, and
**Testcontainers** starters.
Alternatively, clone the
[guide repository](https://github.com/testcontainers/tc-guide-securing-spring-boot-microservice-using-keycloak-and-testcontainers).
After generating the application, add the
[testcontainers-keycloak](https://github.com/dasniko/testcontainers-keycloak)
community module and [REST Assured](https://rest-assured.io/) as test
dependencies.
The key dependencies in `pom.xml` are:
```xml
<properties>
<java.version>17</java.version>
<testcontainers.version>2.0.4</testcontainers.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.dasniko</groupId>
<artifactId>testcontainers-keycloak</artifactId>
<version>3.4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
```
### Create the domain model
Create a `Product` record that represents the domain object:
```java
package com.testcontainers.products.domain;
import jakarta.validation.constraints.NotEmpty;
public record Product(Long id, @NotEmpty String title, String description) {}
```
### Create the repository
Implement `ProductRepository` using Spring `JdbcClient` to interact with a
PostgreSQL database:
```java
package com.testcontainers.products.domain;
import java.util.List;
import org.springframework.jdbc.core.simple.JdbcClient;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Repository;
@Repository
public class ProductRepository {
private final JdbcClient jdbcClient;
public ProductRepository(JdbcClient jdbcClient) {
this.jdbcClient = jdbcClient;
}
public List<Product> getAll() {
return jdbcClient.sql("SELECT * FROM products").query(Product.class).list();
}
public Product create(Product product) {
String sql =
"INSERT INTO products(title, description) VALUES (:title,:description) RETURNING id";
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcClient
.sql(sql)
.param("title", product.title())
.param("description", product.description())
.update(keyHolder);
Long id = keyHolder.getKeyAs(Long.class);
return new Product(id, product.title(), product.description());
}
}
```
### Add a schema creation script
Create `src/main/resources/schema.sql` to initialize the `products` table:
```sql
CREATE TABLE products (
id bigserial primary key,
title varchar not null,
description text
);
```
Enable schema initialization in `src/main/resources/application.properties`:
```properties
spring.sql.init.mode=always
```
For production applications, use a database migration tool like Flyway or
Liquibase instead.
### Implement the API endpoints
Create `ProductController` with endpoints to fetch all products and create a
product:
```java
package com.testcontainers.products.api;
import com.testcontainers.products.domain.Product;
import com.testcontainers.products.domain.ProductRepository;
import jakarta.validation.Valid;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/products")
class ProductController {
private final ProductRepository productRepository;
ProductController(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@GetMapping
List<Product> getAll() {
return productRepository.getAll();
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
Product createProduct(@RequestBody @Valid Product product) {
return productRepository.create(product);
}
}
```
### Configure OAuth 2.0 security
Create a `SecurityConfig` class that protects the API endpoints using JWT
token-based authentication:
```java
package com.testcontainers.products.config;
import static org.springframework.security.config.Customizer.withDefaults;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.CorsConfigurer;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(c ->
c
.requestMatchers(HttpMethod.GET, "/api/products")
.permitAll()
.requestMatchers(HttpMethod.POST, "/api/products")
.authenticated()
.anyRequest()
.authenticated()
)
.sessionManagement(c ->
c.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.cors(CorsConfigurer::disable)
.csrf(CsrfConfigurer::disable)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(withDefaults()));
return http.build();
}
}
```
This configuration:
- Permits unauthenticated access to `GET /api/products`.
- Requires authentication for `POST /api/products` and all other endpoints.
- Configures the OAuth 2.0 Resource Server with JWT token-based authentication.
- Disables CORS and CSRF because this is a stateless API.
Add the JWT issuer URI to `application.properties`:
```properties
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9090/realms/keycloaktcdemo
```
### Export the Keycloak realm configuration
Before writing the tests, export a Keycloak realm configuration so that the test
environment can import it automatically. Start a temporary Keycloak instance:
```console
$ docker run -p 9090:8080 \
-e KEYCLOAK_ADMIN=admin \
-e KEYCLOAK_ADMIN_PASSWORD=admin \
quay.io/keycloak/keycloak:25 start-dev
```
Open `http://localhost:9090` and sign in to the Admin Console with `admin/admin`.
Then set up the realm:
1. In the top-left corner, select the realm drop-down and create a realm named
`keycloaktcdemo`.
2. Under the `keycloaktcdemo` realm, create a client with the following
settings:
- **Client ID**: `product-service`
- **Client Authentication**: `On`
- **Authentication flow**: select only **Service accounts roles**
3. On the **Client details** screen, go to the **Credentials** tab and copy the
**Client secret** value.
Export the realm configuration:
```console
$ docker ps
# copy the keycloak container id
$ docker exec -it <container-id> /bin/bash
$ /opt/keycloak/bin/kc.sh export --dir /opt/keycloak/data/import --realm keycloaktcdemo
$ exit
$ docker cp <container-id>:/opt/keycloak/data/import/keycloaktcdemo-realm.json keycloaktcdemo-realm.json
```
Copy the exported `keycloaktcdemo-realm.json` file into `src/test/resources`.
## Write tests with Testcontainers
To test the secured API endpoints, you need a running Keycloak instance and a
PostgreSQL database, plus a started Spring context. Testcontainers spins up both
services in Docker containers and connects them to Spring through dynamic
property registration.
### Configure the test containers
Spring Boot's Testcontainers support lets you declare containers as beans. For
Keycloak, `@ServiceConnection` isn't available, but you can use
`DynamicPropertyRegistry` to set the JWT issuer URI dynamically.
Create `ContainersConfig.java` under `src/test/java`:
```java
package com.testcontainers.products;
import dasniko.testcontainers.keycloak.KeycloakContainer;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.testcontainers.postgresql.PostgreSQLContainer;
@TestConfiguration(proxyBeanMethods = false)
public class ContainersConfig {
static String POSTGRES_IMAGE = "postgres:16-alpine";
static String KEYCLOAK_IMAGE = "quay.io/keycloak/keycloak:25.0";
static String realmImportFile = "/keycloaktcdemo-realm.json";
static String realmName = "keycloaktcdemo";
@Bean
@ServiceConnection
PostgreSQLContainer postgres() {
return new PostgreSQLContainer(POSTGRES_IMAGE);
}
@Bean
KeycloakContainer keycloak(DynamicPropertyRegistry registry) {
var keycloak = new KeycloakContainer(KEYCLOAK_IMAGE)
.withRealmImportFile(realmImportFile);
registry.add(
"spring.security.oauth2.resourceserver.jwt.issuer-uri",
() -> keycloak.getAuthServerUrl() + "/realms/" + realmName
);
return keycloak;
}
}
```
This configuration:
- Declares a `PostgreSQLContainer` bean with `@ServiceConnection`, which starts
a PostgreSQL container and automatically registers the datasource properties.
- Declares a `KeycloakContainer` bean using the `quay.io/keycloak/keycloak:25.0`
image, imports the realm configuration file, and dynamically registers the JWT
issuer URI from the Keycloak container's auth server URL.
### Write the test
Create `ProductControllerTests.java`:
```java
package com.testcontainers.products.api;
import static io.restassured.RestAssured.given;
import static io.restassured.RestAssured.when;
import static java.util.Collections.singletonList;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.testcontainers.products.ContainersConfig;
import io.restassured.RestAssured;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
@SpringBootTest(webEnvironment = RANDOM_PORT)
@Import(ContainersConfig.class)
class ProductControllerTests {
static final String GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials";
static final String CLIENT_ID = "product-service";
static final String CLIENT_SECRET = "jTJJqdzeCSt3DmypfHZa42vX8U9rQKZ9";
@LocalServerPort
private int port;
@Autowired
OAuth2ResourceServerProperties oAuth2ResourceServerProperties;
@BeforeEach
void setup() {
RestAssured.port = port;
}
@Test
void shouldGetProductsWithoutAuthToken() {
when().get("/api/products").then().statusCode(200);
}
@Test
void shouldGetUnauthorizedWhenCreateProductWithoutAuthToken() {
given()
.contentType("application/json")
.body(
"""
{
"title": "New Product",
"description": "Brand New Product"
}
"""
)
.when()
.post("/api/products")
.then()
.statusCode(401);
}
@Test
void shouldCreateProductWithAuthToken() {
String token = getToken();
given()
.header("Authorization", "Bearer " + token)
.contentType("application/json")
.body(
"""
{
"title": "New Product",
"description": "Brand New Product"
}
"""
)
.when()
.post("/api/products")
.then()
.statusCode(201);
}
private String getToken() {
RestTemplate restTemplate = new RestTemplate();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.put("grant_type", singletonList(GRANT_TYPE_CLIENT_CREDENTIALS));
map.put("client_id", singletonList(CLIENT_ID));
map.put("client_secret", singletonList(CLIENT_SECRET));
String authServerUrl =
oAuth2ResourceServerProperties.getJwt().getIssuerUri() +
"/protocol/openid-connect/token";
var request = new HttpEntity<>(map, httpHeaders);
KeyCloakToken token = restTemplate.postForObject(
authServerUrl,
request,
KeyCloakToken.class
);
assert token != null;
return token.accessToken();
}
record KeyCloakToken(@JsonProperty("access_token") String accessToken) {}
}
```
Here's what the tests cover:
- `shouldGetProductsWithoutAuthToken()` invokes `GET /api/products` without an
`Authorization` header. Because this endpoint is configured to permit
unauthenticated access, the response status code is 200.
- `shouldGetUnauthorizedWhenCreateProductWithoutAuthToken()` invokes the secured
`POST /api/products` endpoint without an `Authorization` header and asserts
the response status code is 401 (Unauthorized).
- `shouldCreateProductWithAuthToken()` first obtains an `access_token` using the
Client Credentials flow. It then includes the token as a Bearer token in the
`Authorization` header when invoking `POST /api/products` and asserts the
response status code is 201 (Created).
The `getToken()` helper method requests an access token from the Keycloak token
endpoint using the client ID and client secret that were configured in the
exported realm.
### Use Testcontainers for local development
Spring Boot's Testcontainers support also works for local development. Create
`TestApplication.java` under `src/test/java`:
```java
package com.testcontainers.products;
import org.springframework.boot.SpringApplication;
public class TestApplication {
public static void main(String[] args) {
SpringApplication
.from(Application::main)
.with(ContainersConfig.class)
.run(args);
}
}
```
Run `TestApplication.java` from your IDE instead of the main `Application.java`.
It starts the containers defined in `ContainersConfig` and configures the
application to use the dynamically registered properties, so you don't have to
install or configure PostgreSQL and Keycloak manually.
## Run tests and next steps
### Run the tests
```console
$ ./mvnw test
```
Or with Gradle:
```console
$ ./gradlew test
```
You should see the Keycloak and PostgreSQL Docker containers start with the
realm settings imported and the tests pass. After the tests finish, the
containers stop and are removed automatically.
### Summary
The Testcontainers Keycloak module lets you develop and test applications using a
real Keycloak server instead of mocks. Testing against a real OAuth 2.0
provider that mirrors your production setup gives you more confidence in your
security configuration and token-based authentication flows.
To learn more about Testcontainers, visit the
[Testcontainers overview](https://testcontainers.com/getting-started/).
### Further reading
- [Getting started with Testcontainers in a Java Spring Boot project](https://testcontainers.com/guides/testing-spring-boot-rest-api-using-testcontainers/)
- [Testcontainers Keycloak module](https://testcontainers.com/modules/keycloak/)
- [testcontainers-keycloak GitHub repository](https://github.com/dasniko/testcontainers-keycloak)
- [Spring Boot OAuth 2.0 Resource Server](https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/index.html)
@@ -1,472 +0,0 @@
---
title: Testing Micronaut Kafka Listener using Testcontainers
linkTitle: Micronaut Kafka
description: Learn how to test a Micronaut Kafka listener using Testcontainers with Kafka and MySQL modules.
keywords: testcontainers, java, micronaut, testing, kafka, mysql, jpa, awaitility
summary: |
Learn how to create a Micronaut application with a Kafka listener that persists data in MySQL,
then test it using Testcontainers Kafka and MySQL modules with Awaitility.
aliases:
- /guides/testcontainers-java-micronaut-kafka/create-project/
- /guides/testcontainers-java-micronaut-kafka/run-tests/
- /guides/testcontainers-java-micronaut-kafka/write-tests/
params:
tags: [testing]
time: 25 minutes
---
<!-- Source: https://github.com/testcontainers/tc-guide-testing-micronaut-kafka-listener -->
In this guide, you'll learn how to:
- Create a Micronaut application with Kafka integration
- Implement a Kafka listener and persist data in a MySQL database
- Test the Kafka listener using Testcontainers and Awaitility
## Prerequisites
- Java 17+
- Maven or Gradle
- A Docker environment supported by Testcontainers
> [!NOTE]
> If you're new to Testcontainers, visit the
> [Testcontainers overview](https://testcontainers.com/getting-started/) to learn more about
> Testcontainers and the benefits of using it.
## Create the Micronaut project
### Set up the project
Create a Micronaut project from [Micronaut Launch](https://micronaut.io/launch) by
selecting the **kafka**, **data-jpa**, **mysql**, **awaitility**, **assertj**, and
**testcontainers** features.
Alternatively, clone the
[guide repository](https://github.com/testcontainers/tc-guide-testing-micronaut-kafka-listener).
You'll use the [Awaitility](http://www.awaitility.org/) library to assert the
expectations of an asynchronous process flow.
The key dependencies in `pom.xml` are:
```xml
<parent>
<groupId>io.micronaut.platform</groupId>
<artifactId>micronaut-parent</artifactId>
<version>4.1.4</version>
</parent>
<dependencies>
<dependency>
<groupId>io.micronaut.data</groupId>
<artifactId>micronaut-data-hibernate-jpa</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.micronaut.kafka</groupId>
<artifactId>micronaut-kafka</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.micronaut.serde</groupId>
<artifactId>micronaut-serde-jackson</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.micronaut.sql</groupId>
<artifactId>micronaut-jdbc-hikari</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>4.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-kafka</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-mysql</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
```
The Micronaut parent POM manages the Testcontainers BOM, so you don't need to
specify versions for Testcontainers modules individually.
### Create the JPA entity
The application listens to a topic called `product-price-changes`. When a
message arrives, it extracts the product code and price from the event payload
and updates the price for that product in the MySQL database.
Create `Product.java`:
```java
package com.testcontainers.demo;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String code;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private BigDecimal price;
public Product() {}
public Product(Long id, String code, String name, BigDecimal price) {
this.id = id;
this.code = code;
this.name = name;
this.price = price;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public BigDecimal getPrice() {
return price;
}
public void setPrice(BigDecimal price) {
this.price = price;
}
}
```
### Create the Micronaut Data JPA repository
Create a repository interface for the `Product` entity with a method to find a
product by code and a method to update the price for a given product code:
```java
package com.testcontainers.demo;
import io.micronaut.data.annotation.Query;
import io.micronaut.data.annotation.Repository;
import io.micronaut.data.jpa.repository.JpaRepository;
import java.math.BigDecimal;
import java.util.Optional;
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
Optional<Product> findByCode(String code);
@Query("update Product p set p.price = :price where p.code = :productCode")
void updateProductPrice(String productCode, BigDecimal price);
}
```
Unlike Spring Data JPA, Micronaut Data uses compile-time annotation processing
to implement repository methods, avoiding runtime reflection.
### Create the event payload
Create a record named `ProductPriceChangedEvent` that represents the structure
of the event payload received from the Kafka topic:
```java
package com.testcontainers.demo;
import io.micronaut.serde.annotation.Serdeable;
import java.math.BigDecimal;
@Serdeable
public record ProductPriceChangedEvent(String productCode, BigDecimal price) {}
```
The `@Serdeable` annotation tells Micronaut Serialization that this type can be
serialized and deserialized.
The sender and receiver agree on the following JSON format:
```json
{
"productCode": "P100",
"price": 25.0
}
```
### Implement the Kafka listener
Create `ProductPriceChangedEventHandler.java`, which handles messages from the
`product-price-changes` topic and updates the product price in the database:
```java
package com.testcontainers.demo;
import static io.micronaut.configuration.kafka.annotation.OffsetReset.EARLIEST;
import io.micronaut.configuration.kafka.annotation.KafkaListener;
import io.micronaut.configuration.kafka.annotation.Topic;
import jakarta.inject.Singleton;
import jakarta.transaction.Transactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
@Transactional
class ProductPriceChangedEventHandler {
private static final Logger LOG = LoggerFactory.getLogger(ProductPriceChangedEventHandler.class);
private final ProductRepository productRepository;
ProductPriceChangedEventHandler(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Topic("product-price-changes")
@KafkaListener(offsetReset = EARLIEST, groupId = "demo")
public void handle(ProductPriceChangedEvent event) {
LOG.info("Received a ProductPriceChangedEvent with productCode:{}: ", event.productCode());
productRepository.updateProductPrice(event.productCode(), event.price());
}
}
```
Key details:
- The `@KafkaListener` annotation marks this class as a Kafka message listener.
Setting `offsetReset` to `EARLIEST` makes the listener start consuming
messages from the beginning of the partition, which is useful during testing.
- The `@Topic` annotation specifies which topic to subscribe to.
- Micronaut handles JSON deserialization of the `ProductPriceChangedEvent`
automatically using Micronaut Serialization.
### Configure the datasource
Add the following properties to `src/main/resources/application.properties`:
```properties
micronaut.application.name=tc-guide-testing-micronaut-kafka-listener
datasources.default.db-type=mysql
datasources.default.dialect=MYSQL
jpa.default.properties.hibernate.hbm2ddl.auto=update
jpa.default.entity-scan.packages=com.testcontainers.demo
datasources.default.driver-class-name=com.mysql.cj.jdbc.Driver
```
Hibernate's `hbm2ddl.auto=update` creates and updates the database schema
automatically. For testing, you'll override this to `create-drop` in the test
properties file.
Create `src/test/resources/application-test.properties`:
```properties
jpa.default.properties.hibernate.hbm2ddl.auto=create-drop
```
## Write tests with Testcontainers
To test the Kafka listener, you need a running Kafka broker and a MySQL
database, plus a started Micronaut application context. Testcontainers spins up
both services in Docker containers and the `TestPropertyProvider` interface
connects them to Micronaut.
### Create a Kafka client for testing
First, create a `@KafkaClient` interface to publish events in the test:
```java
package com.testcontainers.demo;
import io.micronaut.configuration.kafka.annotation.KafkaClient;
import io.micronaut.configuration.kafka.annotation.KafkaKey;
import io.micronaut.configuration.kafka.annotation.Topic;
@KafkaClient
public interface ProductPriceChangesClient {
@Topic("product-price-changes")
void send(@KafkaKey String productCode, ProductPriceChangedEvent event);
}
```
Key details:
- The `@KafkaClient` annotation designates this interface as a Kafka producer.
- The `@Topic` annotation specifies the target topic.
- The `@KafkaKey` annotation marks the parameter used as the Kafka message key.
If no such parameter exists, Micronaut sends the record with a null key.
### Write the test
Create `ProductPriceChangedEventHandlerTest.java`:
```java
package com.testcontainers.demo;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import io.micronaut.context.annotation.Property;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import io.micronaut.test.support.TestPropertyProvider;
import java.math.BigDecimal;
import java.time.Duration;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.testcontainers.kafka.ConfluentKafkaContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@MicronautTest(transactional = false)
@Property(name = "datasources.default.driver-class-name", value = "org.testcontainers.jdbc.ContainerDatabaseDriver")
@Property(name = "datasources.default.url", value = "jdbc:tc:mysql:8.0.32:///db")
@Testcontainers(disabledWithoutDocker = true)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class ProductPriceChangedEventHandlerTest implements TestPropertyProvider {
@Container
static final ConfluentKafkaContainer kafka = new ConfluentKafkaContainer("confluentinc/cp-kafka:7.8.0");
@Override
public @NonNull Map<String, String> getProperties() {
if (!kafka.isRunning()) {
kafka.start();
}
return Collections.singletonMap("kafka.bootstrap.servers", kafka.getBootstrapServers());
}
@Test
void shouldHandleProductPriceChangedEvent(
ProductPriceChangesClient productPriceChangesClient, ProductRepository productRepository) {
Product product = new Product(null, "P100", "Product One", BigDecimal.TEN);
Long id = productRepository.save(product).getId();
ProductPriceChangedEvent event = new ProductPriceChangedEvent("P100", new BigDecimal("14.50"));
productPriceChangesClient.send(event.productCode(), event);
await().pollInterval(Duration.ofSeconds(3)).atMost(10, SECONDS).untilAsserted(() -> {
Optional<Product> optionalProduct = productRepository.findByCode("P100");
assertThat(optionalProduct).isPresent();
assertThat(optionalProduct.get().getCode()).isEqualTo("P100");
assertThat(optionalProduct.get().getPrice()).isEqualTo(new BigDecimal("14.50"));
});
productRepository.deleteById(id);
}
}
```
Here's what the test does:
- `@MicronautTest` initializes the Micronaut application context and the
embedded server. Setting `transactional` to `false` prevents each test method
from running inside a rolled-back transaction, which is necessary because the
Kafka listener processes messages in a separate thread.
- The `@Property` annotations override the datasource driver and URL to use the
Testcontainers special JDBC URL (`jdbc:tc:mysql:8.0.32:///db`). This spins up
a MySQL container and configures it as the datasource automatically.
- `@Testcontainers` and `@Container` manage the Kafka container lifecycle.
The `TestPropertyProvider` interface registers the Kafka bootstrap servers
with Micronaut so that the producer and consumer connect to the test container.
- `@TestInstance(TestInstance.Lifecycle.PER_CLASS)` creates a single test
instance for all test methods, which is required when implementing
`TestPropertyProvider`.
- The test creates a `Product` record in the database, then sends a
`ProductPriceChangedEvent` to the `product-price-changes` topic using the
`ProductPriceChangesClient`.
- Because Kafka message processing is asynchronous, the test uses
[Awaitility](http://www.awaitility.org/) to poll every 3 seconds (up to a
maximum of 10 seconds) until the product price in the database matches the
expected value.
## Run tests and next steps
### Run the tests
```console
$ ./mvnw test
```
Or with Gradle:
```console
$ ./gradlew test
```
You should see the Kafka and MySQL Docker containers start and all tests pass.
After the tests finish, the containers stop and are removed automatically.
### Summary
Testing with real Kafka and MySQL instances gives you more confidence in the
correctness of your code than mocks or embedded alternatives. The
Testcontainers library manages the container lifecycle so that your integration
tests run against the same services you use in production.
To learn more about Testcontainers, visit the
[Testcontainers overview](https://testcontainers.com/getting-started/).
### Further reading
- [Testing REST API integrations in Micronaut apps using WireMock](/guides/testcontainers-java-micronaut-wiremock/)
- [Testing Spring Boot Kafka Listener using Testcontainers](/guides/testcontainers-java-spring-boot-kafka/)
- [Getting started with Testcontainers in a Java Spring Boot project](https://testcontainers.com/guides/testing-spring-boot-rest-api-using-testcontainers/)
- [Awaitility](http://www.awaitility.org/)
- [Testcontainers Kafka module](https://java.testcontainers.org/modules/kafka/)
- [Testcontainers MySQL module](https://java.testcontainers.org/modules/databases/mysql/)
@@ -1,673 +0,0 @@
---
title: Testing REST API integrations in Micronaut apps using WireMock
linkTitle: Micronaut WireMock
description: Learn how to test REST API integrations in a Micronaut application using the Testcontainers WireMock module.
keywords: testcontainers, java, micronaut, testing, wiremock, rest api
summary: |
Learn how to create a Micronaut application that integrates with
external REST APIs, then test those integrations using WireMock
and the Testcontainers WireMock module.
aliases:
- /guides/testcontainers-java-micronaut-wiremock/create-project/
- /guides/testcontainers-java-micronaut-wiremock/run-tests/
- /guides/testcontainers-java-micronaut-wiremock/write-tests/
params:
tags: [testing]
time: 20 minutes
---
<!-- Source: https://github.com/testcontainers/tc-guide-testing-rest-api-integrations-in-micronaut-apps-using-wiremock -->
In this guide, you'll learn how to:
- Create a Micronaut application that talks to external REST APIs
- Test external API integrations using WireMock
- Use the Testcontainers WireMock module to run WireMock as a Docker container
## Prerequisites
- Java 17+
- Maven or Gradle
- A Docker environment supported by Testcontainers
> [!NOTE]
> If you're new to Testcontainers, visit the
> [Testcontainers overview](https://testcontainers.com/getting-started/) to learn more about
> Testcontainers and the benefits of using it.
## Create the Micronaut project
### Set up the project
Create a Micronaut project from [Micronaut Launch](https://micronaut.io/launch)
by selecting the **http-client**, **micronaut-test-rest-assured**, and
**testcontainers** features.
Alternatively, clone the
[guide repository](https://github.com/testcontainers/tc-guide-testing-rest-api-integrations-in-micronaut-apps-using-wiremock).
After generating the project, add the **WireMock** and **Testcontainers
WireMock** libraries as test dependencies. The key dependencies in `pom.xml`
are:
```xml
<parent>
<groupId>io.micronaut.platform</groupId>
<artifactId>micronaut-parent</artifactId>
<version>4.1.2</version>
</parent>
<properties>
<jdk.version>17</jdk.version>
<micronaut.version>4.1.2</micronaut.version>
<micronaut.runtime>netty</micronaut.runtime>
</properties>
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-http-client</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-http-server-netty</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.micronaut.serde</groupId>
<artifactId>micronaut-serde-jackson</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.micronaut.test</groupId>
<artifactId>micronaut-test-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.micronaut.test</groupId>
<artifactId>micronaut-test-rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock-standalone</artifactId>
<version>3.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.wiremock.integrations.testcontainers</groupId>
<artifactId>wiremock-testcontainers-module</artifactId>
<version>1.0-alpha-13</version>
<scope>test</scope>
</dependency>
</dependencies>
```
This guide builds an application that manages video albums. A third-party REST
API handles photo assets. For demonstration purposes, the application uses the
publicly available [JSONPlaceholder](https://jsonplaceholder.typicode.com/) API
as a photo service.
The application exposes a `GET /api/albums/{albumId}` endpoint that calls the
photo service to fetch photos for a given album.
[WireMock](https://wiremock.org/) is a tool for building mock APIs.
Testcontainers provides a
[WireMock module](https://testcontainers.com/modules/wiremock/) that runs
WireMock as a Docker container.
### Create the Album and Photo models
Create `Album.java` using Java records. Annotate both records with `@Serdeable`
to allow serialization and deserialization:
```java
package com.testcontainers.demo;
import io.micronaut.serde.annotation.Serdeable;
import java.util.List;
@Serdeable
public record Album(Long albumId, List<Photo> photos) {}
@Serdeable
record Photo(Long id, String title, String url, String thumbnailUrl) {}
```
### Create the PhotoServiceClient
Micronaut provides
[declarative HTTP client](https://docs.micronaut.io/latest/guide/#httpClient)
support. Create an interface with a method that fetches photos for a given album
ID:
```java
package com.testcontainers.demo;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.PathVariable;
import io.micronaut.http.client.annotation.Client;
import java.util.List;
@Client(id = "photosapi")
interface PhotoServiceClient {
@Get("/albums/{albumId}/photos")
List<Photo> getPhotos(@PathVariable Long albumId);
}
```
The `@Client(id = "photosapi")` annotation ties this client to a named
configuration. Add the following property to
`src/main/resources/application.properties` to set the base URL:
```properties
micronaut.http.services.photosapi.url=https://jsonplaceholder.typicode.com
```
### Create the REST API endpoint
Create `AlbumController.java`:
```java
package com.testcontainers.demo;
import static io.micronaut.scheduling.TaskExecutors.BLOCKING;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.PathVariable;
import io.micronaut.scheduling.annotation.ExecuteOn;
@Controller("/api")
class AlbumController {
private final PhotoServiceClient photoServiceClient;
AlbumController(PhotoServiceClient photoServiceClient) {
this.photoServiceClient = photoServiceClient;
}
@ExecuteOn(BLOCKING)
@Get("/albums/{albumId}")
public Album getAlbumById(@PathVariable Long albumId) {
return new Album(albumId, photoServiceClient.getPhotos(albumId));
}
}
```
Here's what this controller does:
- `@Controller("/api")` maps the controller to the `/api` path.
- Constructor injection provides a `PhotoServiceClient` bean.
- `@ExecuteOn(BLOCKING)` offloads blocking I/O to a separate thread pool so it
doesn't block the event loop.
- `@Get("/albums/{albumId}")` maps the `getAlbumById()` method to an HTTP GET
request.
This endpoint calls the photo service for a given album ID and returns a
response like:
```json
{
"albumId": 1,
"photos": [
{
"id": 51,
"title": "non sunt voluptatem placeat consequuntur rem incidunt",
"url": "https://via.placeholder.com/600/8e973b",
"thumbnailUrl": "https://via.placeholder.com/150/8e973b"
},
{
"id": 52,
"title": "eveniet pariatur quia nobis reiciendis laboriosam ea",
"url": "https://via.placeholder.com/600/121fa4",
"thumbnailUrl": "https://via.placeholder.com/150/121fa4"
}
]
}
```
## Write tests with WireMock and Testcontainers
Mocking external API interactions at the HTTP protocol level, rather than
mocking Java methods, lets you verify marshalling and unmarshalling behavior and
simulate network issues.
### Test with WireMock's JUnit 5 extension
The first approach uses WireMock's `WireMockExtension` to start an in-process
WireMock server on a dynamic port.
Create `AlbumControllerTest.java`:
```java
package com.testcontainers.demo;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.hasSize;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
import io.micronaut.context.ApplicationContext;
import io.micronaut.http.MediaType;
import io.micronaut.runtime.server.EmbeddedServer;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import java.util.Collections;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
class AlbumControllerTest {
@RegisterExtension
static WireMockExtension wireMock = WireMockExtension.newInstance()
.options(wireMockConfig().dynamicPort())
.build();
private Map<String, Object> getProperties() {
return Collections.singletonMap("micronaut.http.services.photosapi.url", wireMock.baseUrl());
}
@Test
void shouldGetAlbumById() {
try (EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class, getProperties())) {
RestAssured.port = server.getPort();
Long albumId = 1L;
String responseJson =
"""
[
{
"id": 1,
"title": "accusamus beatae ad facilis cum similique qui sunt",
"url": "https://via.placeholder.com/600/92c952",
"thumbnailUrl": "https://via.placeholder.com/150/92c952"
},
{
"id": 2,
"title": "reprehenderit est deserunt velit ipsam",
"url": "https://via.placeholder.com/600/771796",
"thumbnailUrl": "https://via.placeholder.com/150/771796"
}
]
""";
wireMock.stubFor(WireMock.get(urlMatching("/albums/" + albumId + "/photos"))
.willReturn(aResponse()
.withHeader("Content-Type", MediaType.APPLICATION_JSON)
.withBody(responseJson)));
given().contentType(ContentType.JSON)
.when()
.get("/api/albums/{albumId}", albumId)
.then()
.statusCode(200)
.body("albumId", is(albumId.intValue()))
.body("photos", hasSize(2));
}
}
@Test
void shouldReturnServerErrorWhenPhotoServiceCallFailed() {
try (EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class, getProperties())) {
RestAssured.port = server.getPort();
Long albumId = 2L;
wireMock.stubFor(WireMock.get(urlMatching("/albums/" + albumId + "/photos"))
.willReturn(aResponse().withStatus(500)));
given().contentType(ContentType.JSON)
.when()
.get("/api/albums/{albumId}", albumId)
.then()
.statusCode(500);
}
}
}
```
Here's what this test does:
- `WireMockExtension` starts a WireMock server on a dynamic port.
- The `getProperties()` method overrides `micronaut.http.services.photosapi.url`
to point at the WireMock endpoint, so the application talks to WireMock
instead of the real photo service.
- `shouldGetAlbumById()` configures a mock response for
`/albums/{albumId}/photos`, sends a request to the application's
`/api/albums/{albumId}` endpoint, and verifies the response body.
- `shouldReturnServerErrorWhenPhotoServiceCallFailed()` configures WireMock to
return a 500 status and verifies the application propagates that error.
### Stub using JSON mapping files
Instead of stubbing with the WireMock Java API, you can use JSON mapping-based
configuration.
Create `src/test/resources/wiremock/mappings/get-album-photos.json`:
```json
{
"mappings": [
{
"request": {
"method": "GET",
"urlPattern": "/albums/([0-9]+)/photos"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"bodyFileName": "album-photos-resp-200.json"
}
},
{
"request": {
"method": "GET",
"urlPattern": "/albums/2/photos"
},
"response": {
"status": 500,
"headers": {
"Content-Type": "application/json"
}
}
},
{
"request": {
"method": "GET",
"urlPattern": "/albums/3/photos"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"jsonBody": []
}
}
]
}
```
Create `src/test/resources/wiremock/__files/album-photos-resp-200.json`:
```json
[
{
"id": 1,
"title": "accusamus beatae ad facilis cum similique qui sunt",
"url": "https://via.placeholder.com/600/92c952",
"thumbnailUrl": "https://via.placeholder.com/150/92c952"
},
{
"id": 2,
"title": "reprehenderit est deserunt velit ipsam",
"url": "https://via.placeholder.com/600/771796",
"thumbnailUrl": "https://via.placeholder.com/150/771796"
}
]
```
Then initialize WireMock to load stub mappings from these files:
```java
@RegisterExtension
static WireMockExtension wireMock = WireMockExtension.newInstance()
.options(
wireMockConfig()
.dynamicPort()
.usingFilesUnderClasspath("wiremock")
)
.build();
```
With mapping files-based stubbing in place, write tests without needing
programmatic stubs:
```java
@Test
void shouldGetAlbumById() {
Long albumId = 1L;
try (EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class, getProperties())) {
RestAssured.port = server.getPort();
given().contentType(ContentType.JSON)
.when()
.get("/api/albums/{albumId}", albumId)
.then()
.statusCode(200)
.body("albumId", is(albumId.intValue()))
.body("photos", hasSize(2));
}
}
```
### Use the Testcontainers WireMock module
The [Testcontainers WireMock module](https://testcontainers.com/modules/wiremock/)
provisions a WireMock server as a standalone container within your tests, based
on [WireMock Docker](https://github.com/wiremock/wiremock-docker).
Create `src/test/resources/mocks-config.json` with the stub mappings:
```json
{
"mappings": [
{
"request": {
"method": "GET",
"urlPattern": "/albums/([0-9]+)/photos"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"bodyFileName": "album-photos-response.json"
}
},
{
"request": {
"method": "GET",
"urlPattern": "/albums/2/photos"
},
"response": {
"status": 500,
"headers": {
"Content-Type": "application/json"
}
}
},
{
"request": {
"method": "GET",
"urlPattern": "/albums/3/photos"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"jsonBody": []
}
}
]
}
```
Create `src/test/resources/album-photos-response.json`:
```json
[
{
"id": 1,
"title": "accusamus beatae ad facilis cum similique qui sunt",
"url": "https://via.placeholder.com/600/92c952",
"thumbnailUrl": "https://via.placeholder.com/150/92c952"
},
{
"id": 2,
"title": "reprehenderit est deserunt velit ipsam",
"url": "https://via.placeholder.com/600/771796",
"thumbnailUrl": "https://via.placeholder.com/150/771796"
}
]
```
Create `AlbumControllerTestcontainersTests.java`:
```java
package com.testcontainers.demo;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.nullValue;
import io.micronaut.context.ApplicationContext;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.runtime.server.EmbeddedServer;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import java.util.Collections;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.wiremock.integrations.testcontainers.WireMockContainer;
@Testcontainers(disabledWithoutDocker = true)
class AlbumControllerTestcontainersTests {
@Container
static WireMockContainer wiremockServer = new WireMockContainer("wiremock/wiremock:2.35.0")
.withMappingFromResource("mocks-config.json")
.withFileFromResource("album-photos-response.json");
@NonNull public Map<String, Object> getProperties() {
return Collections.singletonMap("micronaut.http.services.photosapi.url", wiremockServer.getBaseUrl());
}
@Test
void shouldGetAlbumById() {
Long albumId = 1L;
try (EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class, getProperties())) {
RestAssured.port = server.getPort();
given().contentType(ContentType.JSON)
.when()
.get("/api/albums/{albumId}", albumId)
.then()
.statusCode(200)
.body("albumId", is(albumId.intValue()))
.body("photos", hasSize(2));
}
}
@Test
void shouldReturnServerErrorWhenPhotoServiceCallFailed() {
Long albumId = 2L;
try (EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class, getProperties())) {
RestAssured.port = server.getPort();
given().contentType(ContentType.JSON)
.when()
.get("/api/albums/{albumId}", albumId)
.then()
.statusCode(500);
}
}
@Test
void shouldReturnEmptyPhotos() {
Long albumId = 3L;
try (EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class, getProperties())) {
RestAssured.port = server.getPort();
given().contentType(ContentType.JSON)
.when()
.get("/api/albums/{albumId}", albumId)
.then()
.statusCode(200)
.body("albumId", is(albumId.intValue()))
.body("photos", nullValue());
}
}
}
```
Here's what this test does:
- `@Testcontainers` and `@Container` annotations start a `WireMockContainer`
using the `wiremock/wiremock:2.35.0` Docker image.
- `withMappingFromResource("mocks-config.json")` loads stub mappings from the
classpath resource.
- `withFileFromResource("album-photos-response.json")` makes the response body
file available to WireMock.
- `getProperties()` overrides the photo service URL to point at the WireMock
container's base URL.
- `shouldGetAlbumById()` verifies that the application returns the expected
album with two photos.
- `shouldReturnServerErrorWhenPhotoServiceCallFailed()` verifies that a 500
from the photo service propagates to the caller.
- `shouldReturnEmptyPhotos()` verifies the application handles an empty photo
list.
## Run tests and next steps
### Run the tests
```console
$ ./mvnw test
```
Or with Gradle:
```console
$ ./gradlew test
```
You should see the WireMock Docker container start in the console output. It
acts as the photo service, serving mock responses based on the configured
expectations. All tests should pass.
### Summary
You built a Micronaut application that integrates with an external REST API
using declarative HTTP clients, then tested that integration using WireMock and
the Testcontainers WireMock module. Testing at the HTTP protocol level instead
of mocking Java methods lets you catch serialization issues and simulate
realistic failure scenarios.
> [!TIP]
> Testcontainers WireMock modules are available for Go and Python as well.
To learn more about Testcontainers, visit the
[Testcontainers overview](https://testcontainers.com/getting-started/).
### Further reading
- [Testcontainers WireMock module](https://testcontainers.com/modules/wiremock/)
- [WireMock documentation](https://wiremock.org/docs/)
- [Testcontainers JUnit 5 quickstart](https://java.testcontainers.org/quickstart/junit_5_quickstart/)
- [Testing REST API integrations in Spring Boot using WireMock](/guides/testcontainers-java-wiremock/)
@@ -1,445 +0,0 @@
---
title: Testing REST API integrations using MockServer
linkTitle: MockServer
description: Learn how to test REST API integrations in a Spring Boot application using the Testcontainers MockServer module.
keywords: testcontainers, java, spring boot, testing, mockserver, rest api, rest assured
summary: |
Learn how to create a Spring Boot application that integrates with
external REST APIs, then test those integrations using Testcontainers
and MockServer.
aliases:
- /guides/testcontainers-java-mockserver/create-project/
- /guides/testcontainers-java-mockserver/run-tests/
- /guides/testcontainers-java-mockserver/write-tests/
params:
tags: [testing]
time: 20 minutes
---
<!-- Source: https://github.com/testcontainers/tc-guide-testing-rest-api-integrations-using-mockserver -->
In this guide, you will learn how to:
- Create a Spring Boot application that talks to external REST APIs
- Test external API integrations using the Testcontainers MockServer module
## Prerequisites
- Java 17+
- Maven or Gradle
- A Docker environment supported by Testcontainers
> [!NOTE]
> If you're new to Testcontainers, visit the
> [Testcontainers overview](https://testcontainers.com/getting-started/) to learn more about
> Testcontainers and the benefits of using it.
## Create the Spring Boot project
### Set up the project
Create a Spring Boot project from [Spring Initializr](https://start.spring.io)
by selecting the **Spring Web**, **Spring Reactive Web**, and **Testcontainers**
starters.
Alternatively, clone the
[guide repository](https://github.com/testcontainers/tc-guide-testing-rest-api-integrations-using-mockserver).
After generating the project, add the **REST Assured** and **MockServer**
libraries as test dependencies. The key dependencies in `pom.xml` are:
```xml
<properties>
<java.version>17</java.version>
<testcontainers.version>2.0.4</testcontainers.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-mockserver</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mock-server</groupId>
<artifactId>mockserver-netty</artifactId>
<version>5.15.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
```
Using the Testcontainers BOM (Bill of Materials) is recommended so that you
don't have to repeat the version for every Testcontainers module dependency.
This guide builds an application that manages video albums. A third-party REST
API handles photo assets. For demonstration purposes, the application uses the
publicly available [JSONPlaceholder](https://jsonplaceholder.typicode.com/) API
as a photo service.
The application exposes a `GET /api/albums/{albumId}` endpoint that calls the
photo service to fetch photos for a given album.
[MockServer](https://www.mock-server.com/) is a library for mocking HTTP-based
services. Testcontainers provides a
[MockServer module](https://java.testcontainers.org/modules/mockserver/) that
runs MockServer as a Docker container.
### Create the Album and Photo models
Create `Album.java` using Java records:
```java
package com.testcontainers.demo;
import java.util.List;
public record Album(Long albumId, List<Photo> photos) {}
record Photo(Long id, String title, String url, String thumbnailUrl) {}
```
### Create the PhotoServiceClient interface
Spring Framework 6 introduced
[declarative HTTP client support](https://docs.spring.io/spring-framework/reference/integration/rest-clients.html#rest-http-interface).
Create an interface with a method that fetches photos for a given album ID:
```java
package com.testcontainers.demo;
import java.util.List;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.service.annotation.GetExchange;
interface PhotoServiceClient {
@GetExchange("/albums/{albumId}/photos")
List<Photo> getPhotos(@PathVariable Long albumId);
}
```
### Register PhotoServiceClient as a bean
To generate a runtime implementation of `PhotoServiceClient`, register it as a
Spring bean using `HttpServiceProxyFactory`. The factory requires an
`HttpClientAdapter` implementation. Spring Boot provides `WebClientAdapter` as
part of the `spring-webflux` library:
```java
package com.testcontainers.demo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.support.WebClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
@Configuration
public class AppConfig {
@Bean
public PhotoServiceClient photoServiceClient(
@Value("${photos.api.base-url}") String photosApiBaseUrl
) {
WebClient client = WebClient.builder().baseUrl(photosApiBaseUrl).build();
HttpServiceProxyFactory factory = HttpServiceProxyFactory
.builder(WebClientAdapter.forClient(client))
.build();
return factory.createClient(PhotoServiceClient.class);
}
}
```
The photo service base URL is externalized as a configuration property. Add the
following entry to `src/main/resources/application.properties`:
```properties
photos.api.base-url=https://jsonplaceholder.typicode.com
```
### Create the REST API endpoint
Create `AlbumController.java`:
```java
package com.testcontainers.demo;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClientResponseException;
@RestController
@RequestMapping("/api")
class AlbumController {
private static final Logger logger = LoggerFactory.getLogger(
AlbumController.class
);
private final PhotoServiceClient photoServiceClient;
AlbumController(PhotoServiceClient photoServiceClient) {
this.photoServiceClient = photoServiceClient;
}
@GetMapping("/albums/{albumId}")
public ResponseEntity<Album> getAlbumById(@PathVariable Long albumId) {
try {
List<Photo> photos = photoServiceClient.getPhotos(albumId);
return ResponseEntity.ok(new Album(albumId, photos));
} catch (WebClientResponseException e) {
logger.error("Failed to get photos", e);
return new ResponseEntity<>(e.getStatusCode());
}
}
}
```
This endpoint calls the photo service for a given album ID and returns a
response like:
```json
{
"albumId": 1,
"photos": [
{
"id": 51,
"title": "non sunt voluptatem placeat consequuntur rem incidunt",
"url": "https://via.placeholder.com/600/8e973b",
"thumbnailUrl": "https://via.placeholder.com/150/8e973b"
},
{
"id": 52,
"title": "eveniet pariatur quia nobis reiciendis laboriosam ea",
"url": "https://via.placeholder.com/600/121fa4",
"thumbnailUrl": "https://via.placeholder.com/150/121fa4"
}
]
}
```
## Write tests with Testcontainers MockServer
Mocking external API interactions at the HTTP protocol level, rather than
mocking Java methods, lets you verify marshalling and unmarshalling behavior and
simulate network issues.
Testcontainers provides a MockServer module that starts a
[MockServer](https://www.mock-server.com/) instance inside a Docker container.
You can then use `MockServerClient` to configure mock expectations.
### Write the test
Create `AlbumControllerTest.java`:
```java
package com.testcontainers.demo;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.hasSize;
import static org.mockserver.model.HttpRequest.request;
import static org.mockserver.model.HttpResponse.response;
import static org.mockserver.model.JsonBody.json;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockserver.client.MockServerClient;
import org.mockserver.model.Header;
import org.mockserver.verify.VerificationTimes;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.mockserver.MockServerContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
class AlbumControllerTest {
@LocalServerPort
private Integer port;
@Container
static MockServerContainer mockServerContainer =
new MockServerContainer("mockserver/mockserver:5.15.0");
static MockServerClient mockServerClient;
@DynamicPropertySource
static void overrideProperties(DynamicPropertyRegistry registry) {
mockServerClient =
new MockServerClient(
mockServerContainer.getHost(),
mockServerContainer.getServerPort()
);
registry.add("photos.api.base-url", mockServerContainer::getEndpoint);
}
@BeforeEach
void setUp() {
RestAssured.port = port;
mockServerClient.reset();
}
@Test
void shouldGetAlbumById() {
Long albumId = 1L;
mockServerClient
.when(
request().withMethod("GET").withPath("/albums/" + albumId + "/photos")
)
.respond(
response()
.withStatusCode(200)
.withHeaders(
new Header("Content-Type", "application/json; charset=utf-8")
)
.withBody(
json(
"""
[
{
"id": 1,
"title": "accusamus beatae ad facilis cum similique qui sunt",
"url": "https://via.placeholder.com/600/92c952",
"thumbnailUrl": "https://via.placeholder.com/150/92c952"
},
{
"id": 2,
"title": "reprehenderit est deserunt velit ipsam",
"url": "https://via.placeholder.com/600/771796",
"thumbnailUrl": "https://via.placeholder.com/150/771796"
}
]
"""
)
)
);
given()
.contentType(ContentType.JSON)
.when()
.get("/api/albums/{albumId}", albumId)
.then()
.statusCode(200)
.body("albumId", is(albumId.intValue()))
.body("photos", hasSize(2));
verifyMockServerRequest("GET", "/albums/" + albumId + "/photos", 1);
}
@Test
void shouldReturn404StatusWhenAlbumNotFound() {
Long albumId = 1L;
mockServerClient
.when(
request().withMethod("GET").withPath("/albums/" + albumId + "/photos")
)
.respond(response().withStatusCode(404));
given()
.contentType(ContentType.JSON)
.when()
.get("/api/albums/{albumId}", albumId)
.then()
.statusCode(404);
verifyMockServerRequest("GET", "/albums/" + albumId + "/photos", 1);
}
private void verifyMockServerRequest(String method, String path, int times) {
mockServerClient.verify(
request().withMethod(method).withPath(path),
VerificationTimes.exactly(times)
);
}
}
```
Here's what the test does:
- `@SpringBootTest` starts the full application on a random port.
- The `@Testcontainers` and `@Container` annotations start a
`MockServerContainer` and create a `MockServerClient` connected to it.
- `@DynamicPropertySource` overrides `photos.api.base-url` to point at the
MockServer endpoint, so the application talks to MockServer instead of the
real photo service.
- `@BeforeEach` resets the `MockServerClient` before every test so that
expectations from one test don't affect another.
- `shouldGetAlbumById()` configures a mock response for
`/albums/{albumId}/photos`, sends a request to the application's
`/api/albums/{albumId}` endpoint, and verifies the response body. It also
uses `mockServerClient.verify()` to confirm that the expected API call
reached MockServer.
- `shouldReturn404StatusWhenAlbumNotFound()` configures MockServer to return a
404 status and verifies the application propagates that status to the caller.
## Run tests and next steps
### Run the tests
```console
$ ./mvnw test
```
Or with Gradle:
```console
$ ./gradlew test
```
You should see the MockServer Docker container start in the console output. It
acts as the photo service, serving mock responses based on the configured
expectations. All tests should pass.
### Summary
You built a Spring Boot application that integrates with an external REST API
using declarative HTTP clients, then tested that integration using the
Testcontainers MockServer module. Testing at the HTTP protocol level instead of
mocking Java methods lets you catch serialization issues and simulate realistic
failure scenarios.
To learn more about Testcontainers, visit the
[Testcontainers overview](https://testcontainers.com/getting-started/).
### Further reading
- [Testcontainers MockServer module](https://java.testcontainers.org/modules/mockserver/)
- [MockServer documentation](https://www.mock-server.com/)
- [Testcontainers JUnit 5 quickstart](https://java.testcontainers.org/quickstart/junit_5_quickstart/)
@@ -1,474 +0,0 @@
---
title: Testing Quarkus applications with Testcontainers
linkTitle: Quarkus
description: Learn how to test a Quarkus REST API using Testcontainers with PostgreSQL, Hibernate ORM with Panache, and REST Assured.
keywords: testcontainers, java, quarkus, testing, postgresql, rest api, rest assured, panache, dev services
summary: |
Learn how to create a Quarkus REST API with Hibernate ORM with Panache and PostgreSQL,
then test it using Quarkus Dev Services, Testcontainers, and REST Assured.
aliases:
- /guides/testcontainers-java-quarkus/create-project/
- /guides/testcontainers-java-quarkus/run-tests/
- /guides/testcontainers-java-quarkus/write-tests/
params:
tags: [testing]
time: 25 minutes
---
<!-- Source: https://github.com/testcontainers/tc-guide-testcontainers-in-quarkus-applications -->
In this guide, you'll learn how to:
- Create a Quarkus application with REST API endpoints
- Use Hibernate ORM with Panache and PostgreSQL for persistence
- Test the REST API using Quarkus Dev Services, which uses Testcontainers behind
the scenes
- Test with services not supported by Dev Services using
`QuarkusTestResourceLifecycleManager`
## Prerequisites
- Java 17+
- Maven or Gradle
- A Docker environment supported by Testcontainers
> [!NOTE]
> If you're new to Testcontainers, visit the
> [Testcontainers overview](https://testcontainers.com/getting-started/) to learn more about
> Testcontainers and the benefits of using it.
## Create the Quarkus project
### Set up the project
Create a Quarkus project from [code.quarkus.io](https://code.quarkus.io/) by
selecting the **RESTEasy Classic**, **RESTEasy Classic Jackson**,
**Hibernate Validator**, **Hibernate ORM with Panache**, **JDBC Driver -
PostgreSQL**, and **Flyway** extensions.
Alternatively, clone the
[guide repository](https://github.com/testcontainers/tc-guide-testcontainers-in-quarkus-applications).
The key dependencies in `pom.xml` are:
```xml
<properties>
<quarkus.platform.version>3.22.3</quarkus.platform.version>
</properties>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-flyway</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-validator</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
```
### Create the JPA entity
Hibernate ORM with Panache supports the Active Record pattern and the
Repository pattern to simplify JPA usage. This guide uses the Active Record
pattern.
Create `Customer.java` by extending `PanacheEntity`. This gives the entity
built-in persistence methods such as `persist()`, `listAll()`, and
`findById()`.
```java
package com.testcontainers.demo;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
@Entity
@Table(name = "customers")
public class Customer extends PanacheEntity {
@Column(nullable = false)
public String name;
@Column(nullable = false, unique = true)
public String email;
public Customer() {}
public Customer(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
}
```
### Create the CustomerService CDI bean
Create a `CustomerService` class annotated with `@ApplicationScoped` and
`@Transactional` to handle persistence operations:
```java
package com.testcontainers.demo;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.transaction.Transactional;
import java.util.List;
@ApplicationScoped
@Transactional
public class CustomerService {
public List<Customer> getAll() {
return Customer.listAll();
}
public Customer create(Customer customer) {
customer.persist();
return customer;
}
}
```
### Add the Flyway database migration script
Create `src/main/resources/db/migration/V1__init_database.sql`:
```sql
create sequence customers_seq start with 1 increment by 50;
create table customers
(
id bigint DEFAULT nextval('customers_seq') not null,
name varchar not null,
email varchar not null,
primary key (id)
);
insert into customers(name, email)
values ('john', 'john@mail.com'),
('rambo', 'rambo@mail.com');
```
Enable Flyway migrations in `src/main/resources/application.properties`:
```properties
quarkus.flyway.migrate-at-start=true
```
### Create the REST API endpoints
Create `CustomerResource.java` with endpoints for fetching all customers and
creating a customer:
```java
package com.testcontainers.demo;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.List;
@Path("/api/customers")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class CustomerResource {
private final CustomerService customerService;
public CustomerResource(CustomerService customerService) {
this.customerService = customerService;
}
@GET
public List<Customer> getAllCustomers() {
return customerService.getAll();
}
@POST
public Response createCustomer(Customer customer) {
var savedCustomer = customerService.create(customer);
return Response.status(Response.Status.CREATED).entity(savedCustomer).build();
}
}
```
## Write tests with Testcontainers
### Quarkus Dev Services
Quarkus Dev Services automatically provisions unconfigured services in
development and test mode. When you include an extension and don't configure it,
Quarkus starts the relevant service using
[Testcontainers](https://www.testcontainers.org/) behind the scenes and wires
the application to use that service.
> [!NOTE]
> Dev Services requires a
> [supported Docker environment](https://www.testcontainers.org/supported_docker_environment/).
Quarkus Dev Services supports most commonly used services like SQL databases,
Kafka, RabbitMQ, Redis, and MongoDB. For more information, see the
[Quarkus Dev Services guide](https://quarkus.io/guides/dev-services).
### Write tests for the API endpoints
Test the `GET /api/customers` and `POST /api/customers` endpoints using REST
Assured. The `io.rest-assured:rest-assured` library was already added as a test
dependency when you generated the project.
Create `CustomerResourceTest.java` and annotate it with `@QuarkusTest`. This
bootstraps the application along with the required services using Dev Services.
Because you haven't configured datasource properties, Dev Services automatically
starts a PostgreSQL database using Testcontainers.
```java
package com.testcontainers.demo;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.jupiter.api.Assertions.assertFalse;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.common.mapper.TypeRef;
import io.restassured.http.ContentType;
import java.util.List;
import org.junit.jupiter.api.Test;
@QuarkusTest
class CustomerResourceTest {
@Test
void shouldGetAllCustomers() {
List<Customer> customers = given().when()
.get("/api/customers")
.then()
.statusCode(200)
.extract()
.as(new TypeRef<>() {});
assertFalse(customers.isEmpty());
}
@Test
void shouldCreateCustomerSuccessfully() {
Customer customer = new Customer(null, "John", "john@gmail.com");
given().contentType(ContentType.JSON)
.body(customer)
.when()
.post("/api/customers")
.then()
.statusCode(201)
.body("name", is("John"))
.body("email", is("john@gmail.com"));
}
}
```
Here's what the test does:
- `@QuarkusTest` starts the full Quarkus application with Dev Services enabled.
- Dev Services starts a PostgreSQL container using Testcontainers and configures
the datasource automatically.
- `shouldGetAllCustomers()` calls `GET /api/customers` and verifies that seeded
data from the Flyway migration is returned.
- `shouldCreateCustomerSuccessfully()` sends a `POST /api/customers` request and
verifies the response contains the created customer data.
### Customize test configuration
By default, the Quarkus test instance starts on port 8081 and uses a
`postgres:14` Docker image. Customize both by adding these properties to
`src/main/resources/application.properties`:
```properties
quarkus.http.test-port=0
quarkus.datasource.devservices.image-name=postgres:15.2-alpine
```
Setting `quarkus.http.test-port=0` starts the application on a random available
port, avoiding port conflicts. The `devservices.image-name` property lets you
pin the PostgreSQL image to a specific version that matches production.
### Test with services not supported by Dev Services
Your application might use a service that Dev Services doesn't support out of
the box. In that case, use `QuarkusTestResourceLifecycleManager` to start the
service before the Quarkus application starts for testing.
For example, suppose the application uses CockroachDB. First, add the
CockroachDB Testcontainers module dependency:
```xml
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>cockroachdb</artifactId>
<scope>test</scope>
</dependency>
```
Create a `CockroachDBTestResource` that implements
`QuarkusTestResourceLifecycleManager`:
```java
package com.testcontainers.demo;
import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
import java.util.HashMap;
import java.util.Map;
import org.testcontainers.containers.CockroachContainer;
public class CockroachDBTestResource implements QuarkusTestResourceLifecycleManager {
CockroachContainer cockroachdb;
@Override
public Map<String, String> start() {
cockroachdb = new CockroachContainer("cockroachdb/cockroach:v22.2.0");
cockroachdb.start();
Map<String, String> conf = new HashMap<>();
conf.put("quarkus.datasource.jdbc.url", cockroachdb.getJdbcUrl());
conf.put("quarkus.datasource.username", cockroachdb.getUsername());
conf.put("quarkus.datasource.password", cockroachdb.getPassword());
return conf;
}
@Override
public void stop() {
cockroachdb.stop();
}
}
```
Use the `CockroachDBTestResource` with `@QuarkusTestResource` in a test class:
```java
package com.testcontainers.demo;
import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.assertFalse;
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.common.mapper.TypeRef;
import java.util.List;
import org.junit.jupiter.api.Test;
@QuarkusTest
@QuarkusTestResource(value = CockroachDBTestResource.class, restrictToAnnotatedClass = true)
class CockroachDBTest {
@Test
void shouldGetAllCustomers() {
List<Customer> customers = given().when()
.get("/api/customers")
.then()
.statusCode(200)
.extract()
.as(new TypeRef<>() {});
assertFalse(customers.isEmpty());
}
}
```
The `restrictToAnnotatedClass = true` attribute ensures the CockroachDB
container only starts when running this specific test class, rather than being
activated for all tests.
## Run tests and next steps
### Run the tests
```console
$ ./mvnw test
```
Or with Gradle:
```console
$ ./gradlew test
```
You should see the PostgreSQL Docker container start and all tests pass. After
the tests finish, the container stops and is removed automatically.
### Run the application locally
Quarkus Dev Services automatically provisions unconfigured services in
development mode. Start the Quarkus application in dev mode:
```console
$ ./mvnw compile quarkus:dev
```
Or with Gradle:
```console
$ ./gradlew quarkusDev
```
Dev Services starts a PostgreSQL container automatically. If you're running a
PostgreSQL database on your system and want to use that instead, configure the
datasource properties in `src/main/resources/application.properties`:
```properties
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/postgres
quarkus.datasource.username=postgres
quarkus.datasource.password=postgres
```
When these properties are set explicitly, Dev Services doesn't provision the
database container and instead connects to the configured database.
### Summary
Quarkus Dev Services improves the developer experience by automatically
provisioning the required services using Testcontainers during development and
testing. This guide covered:
- Building a REST API using JAX-RS with Hibernate ORM with Panache
- Testing API endpoints using REST Assured with Dev Services handling database
provisioning
- Using `QuarkusTestResourceLifecycleManager` for services not supported by Dev
Services
- Running the application locally with Dev Services
To learn more about Testcontainers, visit the
[Testcontainers overview](https://testcontainers.com/getting-started/).
### Further reading
- [Quarkus Dev Services overview](https://quarkus.io/guides/dev-services)
- [Quarkus testing guide](https://quarkus.io/guides/getting-started-testing)
- [Testcontainers Postgres module](https://java.testcontainers.org/modules/databases/postgres/)
@@ -1,471 +0,0 @@
---
title: Testing Spring Boot Kafka Listener using Testcontainers
linkTitle: Spring Boot Kafka
description: Learn how to test a Spring Boot Kafka listener using Testcontainers with Kafka and MySQL modules.
keywords: testcontainers, java, spring boot, testing, kafka, mysql, jpa, awaitility
summary: |
Learn how to create a Spring Boot application with a Kafka listener that persists data in MySQL,
then test it using Testcontainers Kafka and MySQL modules with Awaitility.
aliases:
- /guides/testcontainers-java-spring-boot-kafka/create-project/
- /guides/testcontainers-java-spring-boot-kafka/run-tests/
- /guides/testcontainers-java-spring-boot-kafka/write-tests/
params:
tags: [testing]
time: 25 minutes
---
<!-- Source: https://github.com/testcontainers/tc-guide-testing-spring-boot-kafka-listener -->
In this guide, you will learn how to:
- Create a Spring Boot application with Kafka integration
- Implement a Kafka listener and persist data in a MySQL database
- Test the Kafka listener using Testcontainers and Awaitility
## Prerequisites
- Java 17+
- Maven or Gradle
- A Docker environment supported by Testcontainers
> [!NOTE]
> If you're new to Testcontainers, visit the
> [Testcontainers overview](https://testcontainers.com/getting-started/) to learn more about
> Testcontainers and the benefits of using it.
## Create the Spring Boot project
### Set up the project
Create a Spring Boot project from [Spring Initializr](https://start.spring.io)
by selecting the **Spring for Apache Kafka**, **Spring Data JPA**, **MySQL Driver**,
and **Testcontainers** starters.
Alternatively, clone the
[guide repository](https://github.com/testcontainers/tc-guide-testing-spring-boot-kafka-listener).
After generating the application, add the Awaitility library as a test
dependency. You'll use it later to assert the expectations of an asynchronous
process flow.
The key dependencies in `pom.xml` are:
```xml
<properties>
<java.version>17</java.version>
<testcontainers.version>2.0.4</testcontainers.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-kafka</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-mysql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
```
Using the Testcontainers BOM (Bill of Materials) is recommended so that you
don't have to repeat the version for every Testcontainers module dependency.
### Create the JPA entity
The application listens to a topic called `product-price-changes`. When a
message arrives, it extracts the product code and price from the event payload
and updates the price for that product in the MySQL database.
Create `Product.java`:
```java
package com.testcontainers.demo;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
@Entity
@Table(name = "products")
class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String code;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private BigDecimal price;
public Product() {}
public Product(Long id, String code, String name, BigDecimal price) {
this.id = id;
this.code = code;
this.name = name;
this.price = price;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public BigDecimal getPrice() {
return price;
}
public void setPrice(BigDecimal price) {
this.price = price;
}
}
```
### Create the Spring Data JPA repository
Create a repository interface for the `Product` entity with a method to find a
product by code and a method to update the price for a given product code:
```java
package com.testcontainers.demo;
import java.math.BigDecimal;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
interface ProductRepository extends JpaRepository<Product, Long> {
Optional<Product> findByCode(String code);
@Modifying
@Query("update Product p set p.price = :price where p.code = :productCode")
void updateProductPrice(
@Param("productCode") String productCode,
@Param("price") BigDecimal price
);
}
```
### Add a schema creation script
Because the application doesn't use an in-memory database, you need to create
the MySQL tables. The recommended approach for production is a migration tool
like Flyway or Liquibase, but for this guide the built-in Spring Boot schema
initialization is sufficient.
Create `src/main/resources/schema.sql`:
```sql
create table products (
id int NOT NULL AUTO_INCREMENT,
code varchar(255) not null,
name varchar(255) not null,
price numeric(5,2) not null,
PRIMARY KEY (id),
UNIQUE (code)
);
```
Enable schema initialization in `src/main/resources/application.properties`:
```properties
spring.sql.init.mode=always
```
### Create the event payload
Create a record named `ProductPriceChangedEvent` that represents the structure
of the event payload received from the Kafka topic:
```java
package com.testcontainers.demo;
import java.math.BigDecimal;
record ProductPriceChangedEvent(String productCode, BigDecimal price) {}
```
The sender and receiver agree on the following JSON format:
```json
{
"productCode": "P100",
"price": 25.0
}
```
### Implement the Kafka listener
Create `ProductPriceChangedEventHandler.java`, which handles messages from the
`product-price-changes` topic and updates the product price in the database:
```java
package com.testcontainers.demo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Component
@Transactional
class ProductPriceChangedEventHandler {
private static final Logger log = LoggerFactory.getLogger(
ProductPriceChangedEventHandler.class
);
private final ProductRepository productRepository;
ProductPriceChangedEventHandler(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@KafkaListener(topics = "product-price-changes", groupId = "demo")
public void handle(ProductPriceChangedEvent event) {
log.info(
"Received a ProductPriceChangedEvent with productCode:{}: ",
event.productCode()
);
productRepository.updateProductPrice(event.productCode(), event.price());
}
}
```
The `@KafkaListener` annotation specifies the topic name to listen to. Spring
Kafka handles serialization and deserialization based on the properties
configured in `application.properties`.
### Configure Kafka serialization
Add the following Kafka properties to
`src/main/resources/application.properties`:
```properties
######## Kafka Configuration #########
spring.kafka.bootstrap-servers=localhost:9092
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer
spring.kafka.consumer.group-id=demo
spring.kafka.consumer.auto-offset-reset=latest
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.value-deserializer=org.springframework.kafka.support.serializer.JsonDeserializer
spring.kafka.consumer.properties.spring.json.trusted.packages=com.testcontainers.demo
```
The `productCode` key is (de)serialized using `StringSerializer`/`StringDeserializer`,
and the `ProductPriceChangedEvent` value is (de)serialized using
`JsonSerializer`/`JsonDeserializer`.
## Write tests with Testcontainers
To test the Kafka listener, you need a running Kafka broker and a MySQL
database, plus a started Spring context. Testcontainers spins up both services
in Docker containers and `@DynamicPropertySource` connects them to Spring.
### Write the test
Create `ProductPriceChangedEventHandlerTest.java`:
```java
package com.testcontainers.demo;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import java.math.BigDecimal;
import java.time.Duration;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.TestPropertySource;
import org.testcontainers.kafka.ConfluentKafkaContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@SpringBootTest
@TestPropertySource(
properties = {
"spring.kafka.consumer.auto-offset-reset=earliest",
"spring.datasource.url=jdbc:tc:mysql:8.0.32:///db",
}
)
@Testcontainers
class ProductPriceChangedEventHandlerTest {
@Container
static final ConfluentKafkaContainer kafka =
new ConfluentKafkaContainer("confluentinc/cp-kafka:7.8.0");
@DynamicPropertySource
static void overrideProperties(DynamicPropertyRegistry registry) {
registry.add("spring.kafka.bootstrap-servers", kafka::getBootstrapServers);
}
@Autowired
private KafkaTemplate<String, Object> kafkaTemplate;
@Autowired
private ProductRepository productRepository;
@BeforeEach
void setUp() {
Product product = new Product(null, "P100", "Product One", BigDecimal.TEN);
productRepository.save(product);
}
@Test
void shouldHandleProductPriceChangedEvent() {
ProductPriceChangedEvent event = new ProductPriceChangedEvent(
"P100",
new BigDecimal("14.50")
);
kafkaTemplate.send("product-price-changes", event.productCode(), event);
await()
.pollInterval(Duration.ofSeconds(3))
.atMost(10, SECONDS)
.untilAsserted(() -> {
Optional<Product> optionalProduct = productRepository.findByCode(
"P100"
);
assertThat(optionalProduct).isPresent();
assertThat(optionalProduct.get().getCode()).isEqualTo("P100");
assertThat(optionalProduct.get().getPrice())
.isEqualTo(new BigDecimal("14.50"));
});
}
}
```
Here's what the test does:
- `@SpringBootTest` starts the full Spring application context.
- The Testcontainers special JDBC URL (`jdbc:tc:mysql:8.0.32:///db`) in
`@TestPropertySource` spins up a MySQL container and configures it as the
datasource automatically.
- `@Testcontainers` and `@Container` manage the lifecycle of the Kafka
container. `@DynamicPropertySource` registers the Kafka bootstrap servers
with Spring so that the producer and consumer connect to the test container.
- `@BeforeEach` creates a `Product` record in the database before each test.
- The test sends a `ProductPriceChangedEvent` to the `product-price-changes`
topic using `KafkaTemplate`. Spring Boot converts the object to JSON using
`JsonSerializer`.
- Because Kafka message processing is asynchronous, the test uses
[Awaitility](http://www.awaitility.org/) to poll every 3 seconds (up to a
maximum of 10 seconds) until the product price in the database matches the
expected value.
- The property `spring.kafka.consumer.auto-offset-reset` is set to `earliest`
so that the listener consumes messages even if they're sent to the topic
before the listener is ready. This setting is helpful when running tests.
## Run tests and next steps
### Run the tests
```console
$ ./mvnw test
```
Or with Gradle:
```console
$ ./gradlew test
```
You should see the Kafka and MySQL Docker containers start and all tests pass.
After the tests finish, the containers stop and are removed automatically.
### Summary
Testing with real Kafka and MySQL instances gives you more confidence in the
correctness of your code than mocks or embedded alternatives. The
Testcontainers library manages the container lifecycle so that your integration
tests run against the same services you use in production.
To learn more about Testcontainers, visit the
[Testcontainers overview](https://testcontainers.com/getting-started/).
### Further reading
- [Getting started with Testcontainers in a Java Spring Boot project](https://testcontainers.com/guides/testing-spring-boot-rest-api-using-testcontainers/)
- [The simplest way to replace H2 with a real database for testing](https://testcontainers.com/guides/replace-h2-with-real-database-for-testing/)
- [Awaitility](http://www.awaitility.org/)
- [Testcontainers Kafka module](https://java.testcontainers.org/modules/kafka/)
- [Testcontainers MySQL module](https://java.testcontainers.org/modules/databases/mysql/)
@@ -1,770 +0,0 @@
---
title: Testing REST API integrations using WireMock
linkTitle: WireMock
description: Learn how to test REST API integrations in a Spring Boot application using the Testcontainers WireMock module.
keywords: testcontainers, java, spring boot, testing, wiremock, rest api, rest assured
summary: |
Learn how to create a Spring Boot application that integrates with
external REST APIs, then test those integrations using Testcontainers
and WireMock.
aliases:
- /guides/testcontainers-java-wiremock/create-project/
- /guides/testcontainers-java-wiremock/run-tests/
- /guides/testcontainers-java-wiremock/write-tests/
params:
tags: [testing]
time: 20 minutes
---
<!-- Source: https://github.com/testcontainers/tc-guide-testing-rest-api-integrations-using-wiremock -->
In this guide, you'll learn how to:
- Create a Spring Boot application that talks to external REST APIs
- Test external API integrations using WireMock with both the JUnit 5 extension
and the Testcontainers WireMock module
## Prerequisites
- Java 17+
- Maven or Gradle
- A Docker environment supported by Testcontainers
> [!NOTE]
> If you're new to Testcontainers, visit the
> [Testcontainers overview](https://testcontainers.com/getting-started/) to learn more about
> Testcontainers and the benefits of using it.
## Create the Spring Boot project
### Set up the project
Create a Spring Boot project from [Spring Initializr](https://start.spring.io)
by selecting the **Spring Web** and **Testcontainers** starters.
Alternatively, clone the
[guide repository](https://github.com/testcontainers/tc-guide-testing-rest-api-integrations-using-wiremock).
After generating the project, add the **REST Assured**, **WireMock**, and
**WireMock Testcontainers module** libraries as test dependencies. The key
dependencies in `pom.xml` are:
```xml
<properties>
<java.version>17</java.version>
<testcontainers.version>2.0.4</testcontainers.version>
<wiremock-testcontainers.version>1.0-alpha-13</wiremock-testcontainers.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock-standalone</artifactId>
<version>3.6.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.wiremock.integrations.testcontainers</groupId>
<artifactId>wiremock-testcontainers-module</artifactId>
<version>${wiremock-testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
```
Using the Testcontainers BOM (Bill of Materials) is recommended so that you
don't have to repeat the version for every Testcontainers module dependency.
This guide builds an application that manages video albums. A third-party REST
API handles photo assets. For demonstration purposes, the application uses the
publicly available [JSONPlaceholder](https://jsonplaceholder.typicode.com/) API
as a photo service.
The application exposes a `GET /api/albums/{albumId}` endpoint that calls the
photo service to fetch photos for a given album.
[WireMock](https://wiremock.org/) is a tool for building mock APIs.
Testcontainers provides a
[WireMock module](https://testcontainers.com/modules/wiremock/) that runs
WireMock as a Docker container.
### Create the Album and Photo models
Create `Album.java` using Java records:
```java
package com.testcontainers.demo;
import java.util.List;
public record Album(Long albumId, List<Photo> photos) {}
record Photo(Long id, String title, String url, String thumbnailUrl) {}
```
### Create the PhotoServiceClient
Create `PhotoServiceClient.java`, which uses `RestTemplate` to fetch photos for
a given album ID:
```java
package com.testcontainers.demo;
import java.util.List;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
class PhotoServiceClient {
private final String baseUrl;
private final RestTemplate restTemplate;
PhotoServiceClient(
@Value("${photos.api.base-url}") String baseUrl,
RestTemplateBuilder builder
) {
this.baseUrl = baseUrl;
this.restTemplate = builder.build();
}
List<Photo> getPhotos(Long albumId) {
String url = baseUrl + "/albums/{albumId}/photos";
ResponseEntity<List<Photo>> response = restTemplate.exchange(
url,
HttpMethod.GET,
null,
new ParameterizedTypeReference<>() {},
albumId
);
return response.getBody();
}
}
```
The photo service base URL is externalized as a configuration property. Add the
following entry to `src/main/resources/application.properties`:
```properties
photos.api.base-url=https://jsonplaceholder.typicode.com
```
### Create the REST API endpoint
Create `AlbumController.java`:
```java
package com.testcontainers.demo;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestClientResponseException;
@RestController
@RequestMapping("/api")
class AlbumController {
private static final Logger logger = LoggerFactory.getLogger(
AlbumController.class
);
private final PhotoServiceClient photoServiceClient;
AlbumController(PhotoServiceClient photoServiceClient) {
this.photoServiceClient = photoServiceClient;
}
@GetMapping("/albums/{albumId}")
public ResponseEntity<Album> getAlbumById(@PathVariable Long albumId) {
try {
List<Photo> photos = photoServiceClient.getPhotos(albumId);
return ResponseEntity.ok(new Album(albumId, photos));
} catch (RestClientResponseException e) {
logger.error("Failed to get photos", e);
return new ResponseEntity<>(e.getStatusCode());
}
}
}
```
This endpoint calls the photo service for a given album ID and returns a
response like:
```json
{
"albumId": 1,
"photos": [
{
"id": 51,
"title": "non sunt voluptatem placeat consequuntur rem incidunt",
"url": "https://via.placeholder.com/600/8e973b",
"thumbnailUrl": "https://via.placeholder.com/150/8e973b"
},
{
"id": 52,
"title": "eveniet pariatur quia nobis reiciendis laboriosam ea",
"url": "https://via.placeholder.com/600/121fa4",
"thumbnailUrl": "https://via.placeholder.com/150/121fa4"
}
]
}
```
## Write tests with WireMock and Testcontainers
Mocking external API interactions at the HTTP protocol level, rather than
mocking Java methods, lets you verify marshalling and unmarshalling behavior and
simulate network issues.
### Test using WireMock JUnit 5 extension
WireMock provides a JUnit 5 extension that starts an in-process WireMock server.
You can configure stub responses using the WireMock Java API.
Create `AlbumControllerTest.java`:
```java
package com.testcontainers.demo;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.hasSize;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.MediaType;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
@SpringBootTest(webEnvironment = RANDOM_PORT)
class AlbumControllerTest {
@LocalServerPort
private Integer port;
@RegisterExtension
static WireMockExtension wireMock = WireMockExtension
.newInstance()
.options(wireMockConfig().dynamicPort())
.build();
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("photos.api.base-url", wireMock::baseUrl);
}
@BeforeEach
void setUp() {
RestAssured.port = port;
}
@Test
void shouldGetAlbumById() {
Long albumId = 1L;
wireMock.stubFor(
WireMock
.get(urlMatching("/albums/" + albumId + "/photos"))
.willReturn(
aResponse()
.withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.withBody(
"""
[
{
"id": 1,
"title": "accusamus beatae ad facilis cum similique qui sunt",
"url": "https://via.placeholder.com/600/92c952",
"thumbnailUrl": "https://via.placeholder.com/150/92c952"
},
{
"id": 2,
"title": "reprehenderit est deserunt velit ipsam",
"url": "https://via.placeholder.com/600/771796",
"thumbnailUrl": "https://via.placeholder.com/150/771796"
}
]
"""
)
)
);
given()
.contentType(ContentType.JSON)
.when()
.get("/api/albums/{albumId}", albumId)
.then()
.statusCode(200)
.body("albumId", is(albumId.intValue()))
.body("photos", hasSize(2));
}
@Test
void shouldReturnServerErrorWhenPhotoServiceCallFailed() {
Long albumId = 2L;
wireMock.stubFor(
WireMock
.get(urlMatching("/albums/" + albumId + "/photos"))
.willReturn(aResponse().withStatus(500))
);
given()
.contentType(ContentType.JSON)
.when()
.get("/api/albums/{albumId}", albumId)
.then()
.statusCode(500);
}
}
```
Here's what the test does:
- `@SpringBootTest` starts the full application on a random port.
- `@RegisterExtension` creates a `WireMockExtension` that starts WireMock on a
dynamic port.
- `@DynamicPropertySource` overrides `photos.api.base-url` to point at the
WireMock endpoint, so the application talks to WireMock instead of the real
photo service.
- `shouldGetAlbumById()` configures a stub response for
`/albums/{albumId}/photos`, sends a request to the application's
`/api/albums/{albumId}` endpoint, and verifies the response body.
- `shouldReturnServerErrorWhenPhotoServiceCallFailed()` configures WireMock to
return a 500 status and verifies that the application propagates that status to
the caller.
### Stub using JSON mapping files
Instead of using the WireMock Java API, you can configure stubs with JSON
mapping files. Create
`src/test/resources/wiremock/mappings/get-album-photos.json`:
```json
{
"mappings": [
{
"request": {
"method": "GET",
"urlPattern": "/albums/([0-9]+)/photos"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"bodyFileName": "album-photos-resp-200.json"
}
},
{
"request": {
"method": "GET",
"urlPattern": "/albums/2/photos"
},
"response": {
"status": 500,
"headers": {
"Content-Type": "application/json"
}
}
},
{
"request": {
"method": "GET",
"urlPattern": "/albums/3/photos"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"jsonBody": []
}
}
]
}
```
Create the response body file at
`src/test/resources/wiremock/__files/album-photos-resp-200.json`:
```json
[
{
"id": 1,
"title": "accusamus beatae ad facilis cum similique qui sunt",
"url": "https://via.placeholder.com/600/92c952",
"thumbnailUrl": "https://via.placeholder.com/150/92c952"
},
{
"id": 2,
"title": "reprehenderit est deserunt velit ipsam",
"url": "https://via.placeholder.com/600/771796",
"thumbnailUrl": "https://via.placeholder.com/150/771796"
}
]
```
Initialize WireMock to load stubs from the mapping files:
```java
@RegisterExtension
static WireMockExtension wireMockServer = WireMockExtension
.newInstance()
.options(
wireMockConfig().dynamicPort().usingFilesUnderClasspath("wiremock")
)
.build();
```
With mapping-based stubs in place, create
`AlbumControllerWireMockMappingTests.java`:
```java
package com.testcontainers.demo;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.hasSize;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
@SpringBootTest(webEnvironment = RANDOM_PORT)
class AlbumControllerWireMockMappingTests {
@LocalServerPort
private Integer port;
@RegisterExtension
static WireMockExtension wireMockServer = WireMockExtension
.newInstance()
.options(
wireMockConfig().dynamicPort().usingFilesUnderClasspath("wiremock")
)
.build();
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("photos.api.base-url", wireMockServer::baseUrl);
}
@BeforeEach
void setUp() {
RestAssured.port = port;
}
@Test
void shouldGetAlbumById() {
Long albumId = 1L;
given()
.contentType(ContentType.JSON)
.when()
.get("/api/albums/{albumId}", albumId)
.then()
.statusCode(200)
.body("albumId", is(albumId.intValue()))
.body("photos", hasSize(2));
}
@Test
void shouldReturnServerErrorWhenPhotoServiceCallFailed() {
Long albumId = 2L;
given()
.contentType(ContentType.JSON)
.when()
.get("/api/albums/{albumId}", albumId)
.then()
.statusCode(500);
}
@Test
void shouldReturnEmptyPhotos() {
Long albumId = 3L;
given()
.contentType(ContentType.JSON)
.when()
.get("/api/albums/{albumId}", albumId)
.then()
.statusCode(200)
.body("albumId", is(albumId.intValue()))
.body("photos", hasSize(0));
}
}
```
The tests don't need inline stub definitions because WireMock loads the mappings
automatically from the classpath.
### Test using the Testcontainers WireMock module
The [Testcontainers WireMock module](https://testcontainers.com/modules/wiremock/)
provisions WireMock as a standalone Docker container, based on
[WireMock Docker](https://github.com/wiremock/wiremock-docker). This approach is
useful when you want complete isolation between the test JVM and the mock server.
Create a mock configuration file at
`src/test/resources/com/testcontainers/demo/AlbumControllerTestcontainersTests/mocks-config.json`:
```json
{
"mappings": [
{
"request": {
"method": "GET",
"urlPattern": "/albums/([0-9]+)/photos"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"bodyFileName": "album-photos-response.json"
}
},
{
"request": {
"method": "GET",
"urlPattern": "/albums/2/photos"
},
"response": {
"status": 500,
"headers": {
"Content-Type": "application/json"
}
}
},
{
"request": {
"method": "GET",
"urlPattern": "/albums/3/photos"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"jsonBody": []
}
}
]
}
```
Create the response body file at
`src/test/resources/com/testcontainers/demo/AlbumControllerTestcontainersTests/album-photos-response.json`:
```json
[
{
"id": 1,
"title": "accusamus beatae ad facilis cum similique qui sunt",
"url": "https://via.placeholder.com/600/92c952",
"thumbnailUrl": "https://via.placeholder.com/150/92c952"
},
{
"id": 2,
"title": "reprehenderit est deserunt velit ipsam",
"url": "https://via.placeholder.com/600/771796",
"thumbnailUrl": "https://via.placeholder.com/150/771796"
}
]
```
Create `AlbumControllerTestcontainersTests.java`:
```java
package com.testcontainers.demo;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.hasSize;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.wiremock.integrations.testcontainers.WireMockContainer;
@SpringBootTest(webEnvironment = RANDOM_PORT)
@Testcontainers
class AlbumControllerTestcontainersTests {
@LocalServerPort
private Integer port;
@Container
static WireMockContainer wiremockServer = new WireMockContainer(
"wiremock/wiremock:3.6.0"
)
.withMapping(
"photos-by-album",
AlbumControllerTestcontainersTests.class,
"mocks-config.json"
)
.withFileFromResource(
"album-photos-response.json",
AlbumControllerTestcontainersTests.class,
"album-photos-response.json"
);
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("photos.api.base-url", wiremockServer::getBaseUrl);
}
@BeforeEach
void setUp() {
RestAssured.port = port;
}
@Test
void shouldGetAlbumById() {
Long albumId = 1L;
given()
.contentType(ContentType.JSON)
.when()
.get("/api/albums/{albumId}", albumId)
.then()
.statusCode(200)
.body("albumId", is(albumId.intValue()))
.body("photos", hasSize(2));
}
@Test
void shouldReturnServerErrorWhenPhotoServiceCallFailed() {
Long albumId = 2L;
given()
.contentType(ContentType.JSON)
.when()
.get("/api/albums/{albumId}", albumId)
.then()
.statusCode(500);
}
@Test
void shouldReturnEmptyPhotos() {
Long albumId = 3L;
given()
.contentType(ContentType.JSON)
.when()
.get("/api/albums/{albumId}", albumId)
.then()
.statusCode(200)
.body("albumId", is(albumId.intValue()))
.body("photos", hasSize(0));
}
}
```
Here's what the test does:
- The `@Testcontainers` and `@Container` annotations start a
`WireMockContainer` using the `wiremock/wiremock:3.6.0` Docker image.
- `withMapping()` loads stub mappings from `mocks-config.json`, and
`withFileFromResource()` loads the response body file.
- `@DynamicPropertySource` overrides `photos.api.base-url` to point at the
WireMock container's base URL.
- The tests don't contain inline stub definitions because WireMock loads them
from the JSON configuration files.
## Run tests and next steps
### Run the tests
```console
$ ./mvnw test
```
Or with Gradle:
```console
$ ./gradlew test
```
You should see the WireMock Docker container start in the console output. It
acts as the photo service, serving mock responses based on the configured
expectations. All tests should pass.
### Summary
You built a Spring Boot application that integrates with an external REST API,
then tested that integration using three different approaches:
- WireMock JUnit 5 extension with inline stubs
- WireMock JUnit 5 extension with JSON mapping files
- Testcontainers WireMock module running WireMock in a Docker container
Testing at the HTTP protocol level instead of mocking Java methods lets you
catch serialization issues and simulate realistic failure scenarios.
To learn more about Testcontainers, visit the
[Testcontainers overview](https://testcontainers.com/getting-started/).
### Further reading
- [Testcontainers WireMock module](https://testcontainers.com/modules/wiremock/)
- [WireMock documentation](https://wiremock.org/docs/)
- [Testcontainers JUnit 5 quickstart](https://java.testcontainers.org/quickstart/junit_5_quickstart/)
-9
View File
@@ -74,12 +74,3 @@ with different languages and popular frameworks:
- [Replace H2 with a real database for testing](/guides/testcontainers-java-replace-h2/)
- [Configuration of services running in a container](/guides/testcontainers-java-service-configuration/)
- [Testing an ASP.NET Core web app](/guides/testcontainers-dotnet-aspnet-core/)
- [Testing Spring Boot Kafka Listener](/guides/testcontainers-java-spring-boot-kafka/)
- [Testing REST API integrations using MockServer](/guides/testcontainers-java-mockserver/)
- [Testing AWS service integrations using LocalStack](/guides/testcontainers-java-aws-localstack/)
- [Testing Quarkus applications with Testcontainers](/guides/testcontainers-java-quarkus/)
- [Working with jOOQ and Flyway using Testcontainers](/guides/testcontainers-java-jooq-flyway/)
- [Testing REST API integrations using WireMock](/guides/testcontainers-java-wiremock/)
- [Securing Spring Boot with Keycloak and Testcontainers](/guides/testcontainers-java-keycloak-spring-boot/)
- [Testing Micronaut REST API with WireMock](/guides/testcontainers-java-micronaut-wiremock/)
- [Testing Micronaut Kafka Listener](/guides/testcontainers-java-micronaut-kafka/)