Skip to content

Commit e657a92

Browse files
added initial content
1 parent 6a63068 commit e657a92

10 files changed

+545
-28
lines changed

.labspace/01-introduction.md

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
1-
# Introduction
2-
3-
👋 Welcome to the **Labspace starter** lab! During this lab, you will learn to do the following:
4-
5-
- Learning Objective 1
6-
- Learning Objective 2
7-
- Learning Objective 3
8-
- Learning Objective 4
9-
10-
11-
## 🙋 What is a Labspace again?
12-
13-
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis lacinia nisi sit amet auctor accumsan. Maecenas suscipit, libero quis ullamcorper pulvinar, dolor nisl vehicula orci, vel egestas arcu nibh eget enim.
14-
15-
Suspendisse potenti. Pellentesque eleifend eget ante eu egestas.
16-
17-
Nunc sit amet dapibus erat. Aliquam diam arcu, fringilla hendrerit metus sed, pellentesque fringilla lacus.
18-
19-
Nulla ornare nulla risus. Curabitur ut ipsum euismod, accumsan lorem eu, pretium lorem. Fusce imperdiet fermentum hendrerit.
20-
21-
1+
# Step 1: Getting Started
2+
3+
## Clone the following project from GitHub:
4+
```bash
5+
git clone https://github.com/testcontainers/workshop.git
6+
```
7+
8+
## Build the project to download the dependencies
9+
Switch to the workshop folder:
10+
```bash
11+
cd workshop
12+
```
13+
and build the project with Maven:
14+
```bash
15+
./mvnw verify
16+
```
17+
18+
## \(optionally\) Pull the required images before doing the workshop
19+
20+
This might be helpful if the internet connection is somewhat slow.
21+
```bash
22+
docker pull postgres:16-alpine
23+
docker pull redis:7-alpine
24+
docker pull openjdk:8-jre-alpine
25+
docker pull confluentinc/cp-kafka:7.5.0
26+
```

.labspace/02-exploring-the-app.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Step 2: Exploring the app
2+
3+
The app is a simple microservice based on Spring Boot for rating conference talks. It provides an API to track the ratings of the talks in real time.
4+
5+
## Storage
6+
7+
### SQL database with the talks
8+
9+
When a rating is submitted, we must verify that the talk for the given ID is present in our database.
10+
11+
Our database of choice is PostgreSQL, accessed with Spring JDBC.
12+
13+
Check :fileLink[TalksRepository]{path="workshop/src/main/java/com/example/demo/repository/TalksRepository.java"}.
14+
15+
### Redis
16+
17+
We store the ratings in Redis database with Spring Data Redis.
18+
19+
Check :fileLink[RatingsRepository]{path="workshop/src/main/java/com/example/demo/repository/RatingsRepository.java"}.
20+
21+
### Kafka
22+
23+
We use ES/CQRS to materialize the events into the state. Kafka acts as a broker and we use Spring Kafka.
24+
25+
Check :fileLink[RatingsListener]{path="workshop/src/main/java/com/example/demo/streams/RatingsListener.java"}.
26+
27+
## API
28+
29+
The API is a Spring Web REST controller :fileLink[RatingsController]{path="workshop/src/main/java/com/example/demo/api/RatingsController.java"} and exposes two endpoints:
30+
31+
* `POST /ratings { "talkId": ?, "value": 1-5 }` to add a rating for a talk
32+
* `GET /ratings?talkId=?` to get the histogram of ratings of the given talk

.labspace/03-adding-some-tests.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Step 3: Adding some tests
2+
3+
The app doesn't have any tests yet.
4+
But before we write our first test, let's create an abstract test class for the things which are common between the tests.
5+
6+
## Abstract class
7+
8+
Add `AbstractIntegrationTest` class to `src/test/java` sourceset.
9+
It will be an abstract class with standard Spring Boot's testing framework annotations on it:
10+
```plaintext save-as=workshop/src/test/java/com/example/demo/AbstractIntegrationTest.java
11+
package com.example.demo;
12+
13+
import org.springframework.boot.test.context.SpringBootTest;
14+
15+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
16+
public class AbstractIntegrationTest {
17+
18+
}
19+
```
20+
21+
## Our very first test
22+
23+
Now we need to test that the context starts.
24+
Add `DemoApplicationTest`, extend it from your base class `AbstractIntegrationTest` and add a dummy test:
25+
26+
```java save-as=workshop/src/test/java/com/example/demo/DemoApplicationTest.java
27+
package com.example.demo;
28+
29+
import org.junit.jupiter.api.Test;
30+
31+
public class DemoApplicationTest extends AbstractIntegrationTest{
32+
@Test
33+
public void contextLoads() {
34+
}
35+
}
36+
```
37+
38+
Run it and verify that the application starts and the test passes.
39+
```bash
40+
./mvnw clean test
41+
```
42+
Spring will detect H2 on the classpath and use it as an embedded DB.
43+
44+
This is already a useful smoke test since it ensures, that Spring Boot is able to initialize the application context successfully.
45+
46+
## Populate the database
47+
48+
The context starts.
49+
However, we need to populate the database with some data before we can write the tests.
50+
Let's add a `schema.sql` file with the following content:
51+
```sql save-as=workshop/src/test/resources/schema.sql
52+
CREATE TABLE IF NOT EXISTS talks(
53+
id VARCHAR(64) NOT NULL,
54+
title VARCHAR(255) NOT NULL,
55+
PRIMARY KEY (id)
56+
);
57+
58+
INSERT
59+
INTO talks (id, title)
60+
VALUES ('testcontainers-integration-testing', 'Modern Integration Testing with Testcontainers')
61+
ON CONFLICT do nothing;
62+
63+
INSERT
64+
INTO talks (id, title)
65+
VALUES ('flight-of-the-flux', 'A look at Reactor execution model')
66+
ON CONFLICT do nothing;
67+
```
68+
69+
Now run the test again.
70+
```bash
71+
./mvnw clean test
72+
```
73+
Oh no, it fails!
74+
75+
```plaintext
76+
...
77+
Caused by: org.h2.jdbc.JdbcSQLException: Syntax error in SQL statement "INSERT INTO TALKS (ID, TITLE) VALUES ('testcontainers-integration-testing', 'Modern Integration Testing with Testcontainers') ON[*] CONFLICT DO NOTHING";
78+
...
79+
```
80+
81+
It seems that H2 does not support the PostgreSQL SQL syntax, at least not by default.
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Step 4: Your first Testcontainers integration
2+
3+
From the Testcontainers website, we learn that there is a simple way of running different supported JDBC databases with Docker:
4+
[https://www.testcontainers.org/usage/database\_containers.html](https://www.testcontainers.org/usage/database_containers.html)
5+
6+
An especially interesting part are JDBC-URL based containers:
7+
[https://www.testcontainers.org/usage/database\_containers.html\#jdbc-url](https://www.testcontainers.org/usage/database_containers.html#jdbc-url)
8+
9+
It means that starting to use Testcontainers in our project \(once we add a dependency\) is as simple as changing a few properties in Spring Boot:
10+
11+
```java
12+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = {
13+
"spring.datasource.url=jdbc:tc:postgresql:16-alpine://testcontainers/workshop"
14+
})
15+
```
16+
Let's apply it to the `AbstractIntegrationTest` class:
17+
```java save-as=workshop/src/test/java/com/example/demo/AbstractIntegrationTest.java
18+
package com.example.demo;
19+
20+
import org.springframework.boot.test.context.SpringBootTest;
21+
22+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = {
23+
"spring.datasource.url=jdbc:tc:postgresql:16-alpine://testcontainers/workshop"
24+
})
25+
public class AbstractIntegrationTest {
26+
27+
}
28+
```
29+
30+
If we split the magical JDBC url, we see:
31+
32+
* `jdbc:tc:` - this part says that we should use Testcontainers as JDBC provider
33+
* `postgresql:16-alpine://` - we use a PostgreSQL database, and we select the correct PostgreSQL image from the Docker Hub as the image
34+
* `testcontainers/workshop` - the host name \(can be anything\) is `testcontainers` and the database name is `workshop`. Your choice!
35+
36+
## Running tests
37+
In order to run these test we'll need a Docker Engine available. To relax the system ;oad we'll use Testcontainers Cloud to spin up Testcontianers-based containers.
38+
To ebale it you need to:
39+
1. Go to the [app.testcontainers.cloud](https://app.testcontainers.cloud/) and generate the TC_CLOUD_TOKEN.
40+
41+
2. Set the TC_CLOUD_TOKEN as environment variable:
42+
::variableDefinition[tcc_token]{prompt="What is your TC_CLOUD_TOKEN value?"}
43+
44+
```bash
45+
export TC_CLOUD_TOKEN=$$tcc_token$$
46+
```
47+
3. Start Testcontainers Cloud agent
48+
```bash
49+
sh -c "$(curl -fsSL https://get.testcontainers.cloud/bash)"
50+
```
51+
52+
Now run the test again:
53+
```bash
54+
./mvnw clean test
55+
```
56+
Test is green? Good!
57+
58+
Check the logs.
59+
60+
```text
61+
2025-10-27T21:36:55.945Z INFO 77211 --- [ main] o.t.d.DockerClientProviderStrategy : Found Docker environment with Testcontainers Host with tc.host=tcp://127.0.0.1:43387
62+
2025-10-27T21:36:55.946Z INFO 77211 --- [ main] org.testcontainers.DockerClientFactory : Docker host IP address is 127.0.0.1
63+
2025-10-27T21:36:56.055Z INFO 77211 --- [ main] org.testcontainers.DockerClientFactory : Connected to docker:
64+
Server Version: 28.3.3 (via Testcontainers Cloud Agent 1.22.0)
65+
API Version: 1.51
66+
Operating System: Ubuntu 22.04.5 LTS
67+
Total Memory: 31556 MB
68+
2025-10-27T21:36:56.206Z INFO 77211 --- [ main] tc.testcontainers/ryuk:0.8.1 : Creating container for image: testcontainers/ryuk:0.8.1
69+
2025-10-27T21:36:56.396Z INFO 77211 --- [ main] tc.testcontainers/ryuk:0.8.1 : Container testcontainers/ryuk:0.8.1 is starting: 779608b4dc49f2c37420ea0a39cc90951912fb767d7d7141c1b0ae1db1717989
70+
2025-10-27T21:36:57.321Z INFO 77211 --- [ main] tc.testcontainers/ryuk:0.8.1 : Container testcontainers/ryuk:0.8.1 started in PT1.114889292S
71+
2025-10-27T21:36:57.521Z INFO 77211 --- [ main] o.t.utility.RyukResourceReaper : Ryuk started - will monitor and terminate Testcontainers containers on JVM exit
72+
2025-10-27T21:36:57.523Z INFO 77211 --- [ main] org.testcontainers.DockerClientFactory : Checking the system...
73+
2025-10-27T21:36:57.530Z INFO 77211 --- [ main] org.testcontainers.DockerClientFactory : ✔︎ Docker server version should be at least 1.6.0
74+
2025-10-27T21:36:57.532Z INFO 77211 --- [ main] tc.postgres:17-alpine : Creating container for image: postgres:17-alpine
75+
2025-10-27T21:36:57.679Z INFO 77211 --- [ main] tc.postgres:17-alpine : Container postgres:17-alpine is starting: ed1a75d921ab911896763cde925724777aa6cea00700aec567d6b9a293b1e297
76+
2025-10-27T21:36:58.939Z INFO 77211 --- [ main] tc.postgres:17-alpine : Container postgres:17-alpine started in PT1.406803125S
77+
2025-10-27T21:36:58.943Z INFO 77211 --- [ main] tc.postgres:17-alpine : Container is started (JDBC URL: jdbc:postgresql://127.0.0.1:32771/workshop?loggerLevel=OFF)
78+
```
79+
80+
As you can see, Testcontainers quickly discovered your environment and connected to Docker.
81+
It did some pre-flight checks as well to ensure that you have a valid environment.
82+
83+
## Hint:
84+
85+
Changing the PostgreSQL version is as simple as replacing `16-alpine` with, for example, `17-alpine`.
86+
Try it, but don't forget that it will download the new image from the internet, if it's not already present on your computer.
87+
88+
```plaintext save-as=workshop/src/test/java/com/example/demo/AbstractIntegrationTest.java
89+
package com.example.demo;
90+
91+
import org.springframework.boot.test.context.SpringBootTest;
92+
93+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = {
94+
"spring.datasource.url=jdbc:tc:postgresql:17-alpine://testcontainers/workshop"
95+
})
96+
public class AbstractIntegrationTest {
97+
98+
}
99+
```

.labspace/05-dude-r-u-200-ok.md

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# Step 5: Hello, r u 200 OK?
2+
3+
One of the great features of Spring Boot is the Actuator and its health endpoint.
4+
It gives you an overview how healthy your app is.
5+
6+
The context starts, but what's about the health of the app?
7+
8+
## Configure Rest Assured
9+
10+
To check the health endpoint of our app, we will use the [RestAssured](http://rest-assured.io/) library.
11+
12+
However, before using it, we first need to configure it.
13+
Update your abstract test class with `setUpAbstractIntegrationTest` method since we will share it between all tests:
14+
15+
```java save-as=workshop/src/test/java/com/example/demo/AbstractIntegrationTest.java
16+
package com.example.demo;
17+
18+
import io.restassured.RestAssured;
19+
import io.restassured.builder.RequestSpecBuilder;
20+
import io.restassured.specification.RequestSpecification;
21+
import org.junit.jupiter.api.BeforeEach;
22+
import org.springframework.boot.test.context.SpringBootTest;
23+
import org.springframework.boot.test.web.server.LocalServerPort;
24+
import org.springframework.http.MediaType;
25+
import org.testcontainers.shaded.com.google.common.net.HttpHeaders;
26+
27+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = {
28+
"spring.datasource.url=jdbc:tc:postgresql:16-alpine://testcontainers/workshop"
29+
})
30+
public class AbstractIntegrationTest {
31+
protected RequestSpecification requestSpecification;
32+
33+
@LocalServerPort
34+
protected int localServerPort;
35+
36+
@BeforeEach
37+
void setUpAbstractIntegrationTest() {
38+
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
39+
requestSpecification = new RequestSpecBuilder()
40+
.setPort(localServerPort)
41+
.addHeader(
42+
HttpHeaders.CONTENT_TYPE,
43+
MediaType.APPLICATION_JSON_VALUE
44+
)
45+
.build();
46+
}
47+
}
48+
```
49+
50+
Here we ask Spring Boot to inject the random port it received at the start of the app, so that we can pre-configure RestAssured's requestSpecification.
51+
52+
## Call the endpoint
53+
54+
Now let's check if the app is actually healthy.
55+
Add the `healthy` test implementationn in the `DemoApplicationTest` class:
56+
57+
```java save-as=workshop/src/test/java/com/example/demo/DemoApplicationTest.java
58+
package com.example.demo;
59+
60+
import io.restassured.filter.log.LogDetail;
61+
import org.junit.jupiter.api.Test;
62+
import static io.restassured.RestAssured.given;
63+
64+
public class DemoApplicationTest extends AbstractIntegrationTest{
65+
@Test
66+
void healthy() {
67+
given(requestSpecification)
68+
.when()
69+
.get("/actuator/health")
70+
.then()
71+
.statusCode(200)
72+
.log().ifValidationFails(LogDetail.ALL);
73+
}
74+
}
75+
```
76+
77+
Now let's run the test again:
78+
```bash
79+
./mvnw clean test
80+
```
81+
82+
Oh ow! it fails:
83+
84+
```text
85+
...
86+
HTTP/1.1 503 Service Unavailable
87+
transfer-encoding: chunked
88+
Content-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8
89+
90+
{
91+
"status": "DOWN",
92+
"details": {
93+
"diskSpace": { ... },
94+
"redis": {
95+
"status": "DOWN",
96+
"details": {
97+
"error": "org.springframework.data.redis.RedisConnectionFailureException: Unable to connect to Redis; nested exception is io.lettuce.core.RedisConnectionException: Unable to connect to localhost:6379"
98+
}
99+
},
100+
"db": {
101+
"status": "UP",
102+
"details": {
103+
"database": "PostgreSQL",
104+
"hello": 1
105+
}
106+
}
107+
}
108+
}
109+
...
110+
Expected status code <200> but was <503>.
111+
```
112+
113+
It seems that it couldn't find Redis and there is no autoconfigurable option for it.

0 commit comments

Comments
 (0)