Files
Manuel de la Peña b951e92f57 feat(guides): migrate all testcontainers.com guides (#24505)
## Description

Migrate 17 Testcontainers guides from testcontainers.com into the Docker
docs site, covering Java (14 guides), .NET (2 guides), and Node.js (1
guide). This follows up on PR #24450 which added the initial Go and
Python guides.

Each guide is converted from AsciiDoc to Hugo Markdown, split into
multi-chapter stepper navigation, updated to the latest Testcontainers
API, and verified with passing tests running in containers.

Java guides use testcontainers-java 2.0.4 with the new 2.x Maven
coordinates and package names (e.g., `testcontainers-postgresql`,
`org.testcontainers.postgresql.PostgreSQLContainer`). The Quarkus guide
uses Quarkus 3.22.3 with TC 1.x managed by the Quarkus BOM, since no
released Quarkus version ships TC 2.x yet.

## How to test

All code snippets have been verified by running each guide's source
repository tests inside Docker containers with the Docker socket
mounted.

To re-run the verification, use the `/testcontainers-guides-migrator`
skill included in this PR
(`.claude/skills/testcontainers-guides-migrator/SKILL.md`). The skill's
Step 6 documents the exact container commands and macOS Docker Desktop
workarounds (host override, docker-java API version, etc.) needed to run
each language's tests:

```
/testcontainers-guides-migrator I want you to verify all the guides in this branch.
Do a full review, verifying that all code snippets compile, the code is executable,
and ALL the tests pass. Run them as docker containers, never locally.
```

## Related issues or tickets

Supersedes #24450 (expanded from 2 guides to all 19)

## Reviews

- [ ] Technical review
- [ ] Editorial review
- [ ] Product review

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 10:03:26 +00:00

229 lines
7.4 KiB
Markdown

---
title: Write tests with Testcontainers
linkTitle: Write tests
description: Test the secured Spring Boot API endpoints using Testcontainers Keycloak and PostgreSQL modules.
weight: 20
---
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.