This example shows the way you could use Testcontainers with JUnit 4.
!!! note JUnit 4 is in maintenance mode since 2025-05-31, so we recommend using JUnit 5 or newer versions instead.
Let's imagine we have a simple program that has a dependency on Redis, and we want to add some tests for it.
In our imaginary program, there is a RedisBackedCache class which stores data in Redis.
You can see an example test that could have been written for it (without using Testcontainers):
Pre-Testcontainers test code block:RedisBackedCacheIntTestStep0
Notice that the existing test has a problem - it's relying on a local installation of Redis, which is a red flag for test reliability. This may work if we were sure that every developer and CI machine had Redis installed, but would fail otherwise. We might also have problems if we attempted to run tests in parallel, such as state bleeding between tests, or port clashes.
Let's start from here, and see how to improve the test with Testcontainers:
First, add Testcontainers as a dependency as follows:
=== "Gradle"
groovy testImplementation("org.testcontainers:testcontainers:{{latest_version}}") testImplementation("org.testcontainers:junit-vintage:{{latest_version}}")
=== "Maven"
xml <dependency> <groupId>org.testcontainers</groupId> <artifactId>testcontainers</artifactId> <version>{{latest_version}}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>junit-vintage</artifactId> <version>{{latest_version}}</version> <scope>test</scope> </dependency>
Add the following to the body of our test class:
JUnit 4 Rule inside_block:rule
The @Rule annotation tells JUnit to notify this field about various events in the test lifecycle.
Wrap the containers with new TestContainersRule(...) so the containers start and stop, according to the test lifecycle.
In this case, our rule object is not static, so the container will start and stop with every test.
The test configures GenericContainer to use a specific Redis image from Docker Hub, and to expose a port.
If we run our test as-is, then regardless of the actual test outcome, we'll see logs showing us that Testcontainers:
- was activated before our test method ran
- discovered and quickly tested our local Docker setup
- pulled the image if necessary
- started a new container and waited for it to be ready
- shut down and deleted the container after the test
Before Testcontainers, we might have hardcoded an address like localhost:6379 into our tests.
Testcontainers uses randomized ports for each container it starts, but makes it easy to obtain the actual port at runtime.
We can do this in our test setUp method, to set up our component under test:
Obtaining a mapped port inside_block:setUp
!!! tip
Notice that we also ask Testcontainers for the container's actual address with redis.getHost();,
rather than hard-coding localhost. localhost may work in some environments but not others - for example it may
not work on your current or future CI environment. As such, avoid hard-coding the address, and use
getHost() instead.
That's it!
Let's look at our complete test class to see how little we had to add to get up and running with Testcontainers:
RedisBackedCacheIntTest block:RedisBackedCacheIntTest