“But locally everything went green!”
We all know this pain: your pipeline is green, your tests run locally in milliseconds. You deploy to staging or production – and it works. SQLSyntaxErrorException. Or worse: data is stored incorrectly.
The reason? Your tests run against a H2 in-memory database, but your app runs against a real PostgreSQL (or MySQL/Oracle).
For years we told ourselves: “Oh, H2 behaves almost like Postgres.” Spoiler: It doesn’t. And as a senior engineer, I tell you: Stop mocking your database. Test against the real thing.
This is where Testcontainers comes into play.
The problem with the “fake” database
Why do we even use H2? Because it’s comfortable. No installation, super fast, starts with the app. But the price you pay is False Confidence.
- Syntax differences: H2 does not support all Postgres JSONB features.
- Dialects: A query that works in H2 can crash in Postgres (and vice versa).
- Constraints: Triggers and stored procedures often behave differently.
When you use H2, you don’t test your database logic. You test whether Hibernate works. That’s too little.
The solution: Docker in the JUnit test
Testcontainers is a Java library that allows you to start Docker containers directly from your JUnit test. When you start your tests, the following happens:
- Java spins up a real PostgreSQL instance in Docker.
- Your Spring Boot app dynamically connects against this container.
- The tests run against the real DB.
- After the test, the container is destroyed.
No manual docker-compose up before testing. No leaky data from previous test runs. A fresh, real environment every time.
The implementation (The modern way with Spring Boot 3.1+)
Test containers used to be a bit fiddly to configure (set dynamic ports manually, etc.). Since Spring Boot 3.1 it’s almost magical thanks to @ServiceConnection.
Here is the code you should write today.
Step 1: Dependencies (Maven)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
Step 2: The Test Setup
Forget complicated application-test.yml files. We define the container directly in the test:
@SpringBootTest
@Testcontainers
class OrderIntegrationTest {
// We define: We want a real Postgres 15 image
@Container
@ServiceConnection // <- The magic: Spring injects URL/User/Password automatically!
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine");
@Autowired
private OrderRepository orderRepository;
@Test
void shouldSaveAndFindOrder() {
Order order = new Order("MacBook Pro", 9999.00);
orderRepository.save(order);
var foundOrder = orderRepository.findByName("MacBook Pro");
assertThat(foundOrder).isPresent();
// This test ran against a real DB!
}
}
That’s it. Spring Boot recognizes the container, sees the @ServiceConnection annotation and automatically overwrites the DataSource properties. You don’t have to worry about ports or passwords.
A word about speed
“But Julian, that takes a lot longer!”
Yes, starting a Docker container takes 2-3 seconds. H2 lasts 200 milliseconds. But integration tests aren’t designed to run while typing (that’s what you have unit tests for). Integration testing is your safety net.
And let’s be honest: what takes more time? A) Wait 5 seconds longer for the tests? B) 5 hours of debugging because the SQL query fails in production even though the test was green?
Quality beats speed. Always.
Conclusion: Reality instead of simulation
With test containers you bring production parity to your development environment. You eliminate an entire class of errors (“It works on H2”).
By the way, it doesn’t just work for databases. You can spin up Redis, Kafka, Elasticsearch or even LocalStack (AWS) as a container.
My advice: Banish H2 from your integration tests. Your nerves (and your users) will thank you.
Happy Testing! 🧪
![[EN] Nie wieder 'Works on my Machine': Datenbank-Tests richtig gemacht](/images/Testcontainers-header.jpeg)