Skip to content

Create junit-vintage module #10351

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ dependencies {
testImplementation 'redis.clients:jedis:5.1.5'
testImplementation 'com.rabbitmq:amqp-client:5.22.0'
testImplementation 'org.mongodb:mongo-java-driver:3.12.14'
testImplementation project(':junit-vintage')

testImplementation ('org.mockito:mockito-core:4.11.0') {
exclude(module: 'hamcrest-core')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
import org.junit.runner.RunWith;
import org.testcontainers.DockerClientFactory;
import org.testcontainers.TestImages;
import org.testcontainers.junit.vintage.Container;
import org.testcontainers.junit.vintage.TemporaryNetwork;
import org.testcontainers.junit.vintage.Testcontainers;

import static org.assertj.core.api.Assertions.assertThat;

Expand All @@ -15,15 +18,18 @@ public class NetworkTest {
public static class WithRules {

@Rule
public Network network = Network.newNetwork();
public TemporaryNetwork network = new TemporaryNetwork(Network.newNetwork());

@Rule
public Testcontainers containers = new Testcontainers(this);

@Container
public GenericContainer<?> foo = new GenericContainer<>(TestImages.TINY_IMAGE)
.withNetwork(network)
.withNetworkAliases("foo")
.withCommand("/bin/sh", "-c", "while true ; do printf 'HTTP/1.1 200 OK\\n\\nyay' | nc -l -p 8080; done");

@Rule
@Container
public GenericContainer<?> bar = new GenericContainer<>(TestImages.TINY_IMAGE)
.withNetwork(network)
.withCommand("top");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@

import org.junit.Rule;
import org.testcontainers.containers.ComposeContainer;
import org.testcontainers.junit.vintage.Container;
import org.testcontainers.junit.vintage.Testcontainers;

import java.io.File;

public class ComposeContainerPortViaEnvTest extends BaseComposeTest {

@Rule
public final Testcontainers containers = new Testcontainers(this);

@Container
public ComposeContainer environment = new ComposeContainer(
new File("src/test/resources/v2-compose-test-port-via-env.yml")
)
Expand Down
1 change: 1 addition & 0 deletions docs/examples/junit4/redis/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ dependencies {

testImplementation "junit:junit:4.13.2"
testImplementation project(":testcontainers")
testImplementation project(":junit-vintage")
testImplementation 'org.assertj:assertj-core:3.26.3'
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import org.junit.Rule;
import org.junit.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.junit.vintage.Container;
import org.testcontainers.junit.vintage.Testcontainers;
import org.testcontainers.utility.DockerImageName;

import static org.assertj.core.api.Assertions.assertThat;
Expand All @@ -14,13 +16,19 @@ public class RedisBackedCacheIntTest {

// rule {
@Rule
public Testcontainers containers = new Testcontainers(this);

// }

// container {
@Container
public GenericContainer redis = new GenericContainer(DockerImageName.parse("redis:6-alpine"))
.withExposedPorts(6379);

// }

@Before
public void setUp() {
public void createCache() {
String address = redis.getHost();
Integer port = redis.getFirstMappedPort();

Expand All @@ -29,7 +37,7 @@ public void setUp() {
}

@Test
public void testSimplePutAndGet() {
public void simplePutAndGet() {
underTest.put("test", "example");

String retrieved = underTest.get("test");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ public class RedisBackedCacheIntTestStep0 {
private RedisBackedCache underTest;

@Before
public void setUp() {
public void createCache() {
// Assume that we have Redis running locally?
underTest = new RedisBackedCache("localhost", 6379);
}

@Test
public void testSimplePutAndGet() {
public void simplePutAndGet() {
underTest.put("test", "example");

String retrieved = underTest.get("test");
Expand Down
29 changes: 20 additions & 9 deletions docs/quickstart/junit_4_quickstart.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
# JUnit 4 Quickstart

It's easy to add Testcontainers to your project - let's walk through a quick example to see how.
This example shows the way you could use Testcontainers with JUnit 4.

!!! note
JUnit 4 is in [maintenance mode since 2025-05-31](https://github.com/junit-team/junit4), 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):

<!--codeinclude-->
Expand All @@ -15,15 +18,16 @@ Notice that the existing test has a problem - it's relying on a local installati
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:
Let's start from here, and see how to improve the test with Testcontainers:

## 1. Add Testcontainers as a test-scoped dependency

First, add Testcontainers as a dependency as follows:

=== "Gradle"
```groovy
testImplementation "org.testcontainers:testcontainers:{{latest_version}}"
testImplementation("org.testcontainers:testcontainers:{{latest_version}}")
testImplementation("org.testcontainers:junit-vintage:{{latest_version}}")
```
=== "Maven"
```xml
Expand All @@ -33,18 +37,26 @@ First, add Testcontainers as a dependency as follows:
<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>
```

## 2. Get Testcontainers to run a Redis container during our tests

Simply add the following to the body of our test class:
Add the following to the body of our test class:

<!--codeinclude-->
[JUnit 4 Rule](../examples/junit4/redis/src/test/java/quickstart/RedisBackedCacheIntTest.java) inside_block:rule
<!--/codeinclude-->

The `@Rule` annotation tells JUnit to notify this field about various events in the test lifecycle.
In this case, our rule object is a Testcontainers `GenericContainer`, configured to use a specific Redis image from Docker Hub, and configured to expose a port.
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:

Expand All @@ -66,9 +78,9 @@ We can do this in our test `setUp` method, to set up our component under test:
<!--/codeinclude-->

!!! tip
Notice that we also ask Testcontainers for the container's actual address with `redis.getHost();`,
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
not work on your current or future CI environment. As such, **avoid hard-coding** the address, and use
`getHost()` instead.

## 4. Run the tests!
Expand All @@ -80,4 +92,3 @@ Let's look at our complete test class to see how little we had to add to get up
<!--codeinclude-->
[RedisBackedCacheIntTest](../examples/junit4/redis/src/test/java/quickstart/RedisBackedCacheIntTest.java) block:RedisBackedCacheIntTest
<!--/codeinclude-->

11 changes: 11 additions & 0 deletions modules/junit-vintage/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
description = "Testcontainers :: JUnit4 Rule"

dependencies {
api project(":testcontainers")
api 'junit:junit:4.13.2'
implementation platform('org.junit:junit-bom:5.10.3')
implementation 'org.junit.jupiter:junit-jupiter-api'

testImplementation 'com.datastax.oss:java-driver-core:4.17.0'
testImplementation 'org.assertj:assertj-core:3.26.3'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.testcontainers.junit.vintage;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* The {@code @Container} annotation marks containers that should be managed by the
* {@code Testcontainers} rule.
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Container {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package org.testcontainers.junit.vintage;

import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.MultipleFailureException;
import org.junit.runners.model.Statement;
import org.testcontainers.lifecycle.TestDescription;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

/**
* {@link TestRule} which is called before and after each test, and also is notified on success/failure.
*
* This mimics the behaviour of TestWatcher to some degree, but failures occurring in {@code starting()}
* prevent the test from being run.
*/
class FailureDetectingExternalResource implements TestRule {

@Override
public final Statement apply(Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
List<Throwable> errors = new ArrayList<Throwable>();
Optional<Throwable> failure = Optional.empty();

try {
starting(description);
base.evaluate();
notifySucceeded(description, errors);
} catch (org.junit.internal.AssumptionViolatedException e) {
failure = Optional.of(e);
} catch (Throwable e) {
failure = Optional.of(e);
errors.add(e);
notifyFailed(e, description);
} finally {
notifyFinished(failure, description, errors);
}

MultipleFailureException.assertEmpty(errors);
}
};
}

protected void starting(Description description) throws Throwable {}

protected void succeeded(Description description) throws Throwable {}

protected void failed(Throwable e, Description description) throws Throwable {}

protected void finished(Description description) throws Throwable {}

private void notifySucceeded(Description description, List<Throwable> errors) {
try {
succeeded(description);
} catch (Throwable e) {
errors.add(e);
}
}

private void notifyFailed(Throwable failure, Description description) {
try {
failed(failure, description);
} catch (Throwable e) {
failure.addSuppressed(e);
}
}

private void notifyFinished(Optional<Throwable> failure, Description description, List<Throwable> errors) {
try {
finished(description);
} catch (Throwable e) {
failure.ifPresent(f -> f.addSuppressed(e)); // ifPresentOrElse() requires Java 9
if (!failure.isPresent()) {
errors.add(e);
}
}
}

protected static final TestDescription toTestDescription(Description description) {
return new TestDescription() {
@Override
public String getTestId() {
return description.getDisplayName();
}

@Override
public String getFilesystemFriendlyName() {
return description.getClassName() + "-" + description.getMethodName();
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package org.testcontainers.junit.vintage;

import org.junit.rules.ExternalResource;
import org.testcontainers.containers.Network;

/**
* Integrates {@link Network} with the JUnit4 lifecycle.
*/
public final class TemporaryNetwork extends ExternalResource implements Network {

private final Network network;

private volatile State state = State.BEFORE_RULE;

/**
* Creates an instance.
*
* <p>The passed-in network will be closed when the current test completes.
*
* @param network Network that the rule will delegate to.
*/
public TemporaryNetwork(Network network) {
this.network = network;
}

@Override
public String getId() {
if (state == State.AFTER_RULE) {
throw new IllegalStateException("Cannot reference the network after the test completes");
}
return network.getId();
}

@Override
public void close() {
switch (state) {
case BEFORE_RULE:
throw new IllegalStateException("Cannot close the network before the test starts");
case INSIDE_RULE:
break;
case AFTER_RULE:
throw new IllegalStateException("Cannot reference the network after the test completes");
}
network.close();
}

@Override
protected void before() throws Throwable {
state = State.AFTER_RULE; // Just in case an exception is thrown below.
network.getId(); // This has the side-effect of creating the network.

state = State.INSIDE_RULE;
}

@Override
protected void after() {
state = State.AFTER_RULE;
network.close();
}

private enum State {
BEFORE_RULE,
INSIDE_RULE,
AFTER_RULE,
}
}
Loading