Skip to content

Commit 8264c3b

Browse files
committed
Create Testcontainers Rule
1 parent 67b682a commit 8264c3b

File tree

12 files changed

+564
-14
lines changed

12 files changed

+564
-14
lines changed

core/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ dependencies {
113113
testImplementation 'redis.clients:jedis:5.1.5'
114114
testImplementation 'com.rabbitmq:amqp-client:5.22.0'
115115
testImplementation 'org.mongodb:mongo-java-driver:3.12.14'
116+
testImplementation project(':junit-vintage')
116117

117118
testImplementation ('org.mockito:mockito-core:4.11.0') {
118119
exclude(module: 'hamcrest-core')

core/src/test/java/org/testcontainers/containers/NetworkTest.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import org.junit.runner.RunWith;
77
import org.testcontainers.DockerClientFactory;
88
import org.testcontainers.TestImages;
9+
import org.testcontainers.junit.vintage.Container;
10+
import org.testcontainers.junit.vintage.Testcontainers;
911

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

@@ -18,12 +20,15 @@ public static class WithRules {
1820
public Network network = Network.newNetwork();
1921

2022
@Rule
23+
public Testcontainers containers = new Testcontainers(this);
24+
25+
@Container
2126
public GenericContainer<?> foo = new GenericContainer<>(TestImages.TINY_IMAGE)
2227
.withNetwork(network)
2328
.withNetworkAliases("foo")
2429
.withCommand("/bin/sh", "-c", "while true ; do printf 'HTTP/1.1 200 OK\\n\\nyay' | nc -l -p 8080; done");
2530

26-
@Rule
31+
@Container
2732
public GenericContainer<?> bar = new GenericContainer<>(TestImages.TINY_IMAGE)
2833
.withNetwork(network)
2934
.withCommand("top");

docs/examples/junit4/redis/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ dependencies {
55

66
testImplementation "junit:junit:4.13.2"
77
testImplementation project(":testcontainers")
8+
testImplementation project(":junit-vintage")
89
testImplementation 'org.assertj:assertj-core:3.26.3'
910
}
1011

docs/examples/junit4/redis/src/test/java/quickstart/RedisBackedCacheIntTest.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import org.junit.Rule;
55
import org.junit.Test;
66
import org.testcontainers.containers.GenericContainer;
7+
import org.testcontainers.junit.vintage.Container;
8+
import org.testcontainers.junit.vintage.Testcontainers;
79
import org.testcontainers.utility.DockerImageName;
810

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

1517
// rule {
1618
@Rule
19+
public Testcontainers containers = new Testcontainers(this);
20+
21+
// }
22+
23+
// container {
24+
@Container
1725
public GenericContainer redis = new GenericContainer(DockerImageName.parse("redis:6-alpine"))
1826
.withExposedPorts(6379);
1927

2028
// }
2129

2230
@Before
23-
public void setUp() {
31+
public void createCache() {
2432
String address = redis.getHost();
2533
Integer port = redis.getFirstMappedPort();
2634

@@ -29,7 +37,7 @@ public void setUp() {
2937
}
3038

3139
@Test
32-
public void testSimplePutAndGet() {
40+
public void simplePutAndGet() {
3341
underTest.put("test", "example");
3442

3543
String retrieved = underTest.get("test");

docs/examples/junit4/redis/src/test/java/quickstart/RedisBackedCacheIntTestStep0.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ public class RedisBackedCacheIntTestStep0 {
1212
private RedisBackedCache underTest;
1313

1414
@Before
15-
public void setUp() {
15+
public void createCache() {
1616
// Assume that we have Redis running locally?
1717
underTest = new RedisBackedCache("localhost", 6379);
1818
}
1919

2020
@Test
21-
public void testSimplePutAndGet() {
21+
public void simplePutAndGet() {
2222
underTest.put("test", "example");
2323

2424
String retrieved = underTest.get("test");

docs/quickstart/junit_4_quickstart.md

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
# JUnit 4 Quickstart
22

3-
It's easy to add Testcontainers to your project - let's walk through a quick example to see how.
3+
This example shows the way you could use Testcontainers with JUnit 4.
4+
5+
!!! note
6+
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.
47

58
Let's imagine we have a simple program that has a dependency on Redis, and we want to add some tests for it.
69
In our imaginary program, there is a `RedisBackedCache` class which stores data in Redis.
7-
10+
811
You can see an example test that could have been written for it (without using Testcontainers):
912

1013
<!--codeinclude-->
@@ -15,15 +18,16 @@ Notice that the existing test has a problem - it's relying on a local installati
1518
This may work if we were sure that every developer and CI machine had Redis installed, but would fail otherwise.
1619
We might also have problems if we attempted to run tests in parallel, such as state bleeding between tests, or port clashes.
1720

18-
Let's start from here, and see how to improve the test with Testcontainers:
21+
Let's start from here, and see how to improve the test with Testcontainers:
1922

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

2225
First, add Testcontainers as a dependency as follows:
2326

2427
=== "Gradle"
2528
```groovy
26-
testImplementation "org.testcontainers:testcontainers:{{latest_version}}"
29+
testImplementation("org.testcontainers:testcontainers:{{latest_version}}")
30+
testImplementation("org.testcontainers:junit-vintage:{{latest_version}}")
2731
```
2832
=== "Maven"
2933
```xml
@@ -33,18 +37,26 @@ First, add Testcontainers as a dependency as follows:
3337
<version>{{latest_version}}</version>
3438
<scope>test</scope>
3539
</dependency>
40+
<dependency>
41+
<groupId>org.testcontainers</groupId>
42+
<artifactId>junit-vintage</artifactId>
43+
<version>{{latest_version}}</version>
44+
<scope>test</scope>
45+
</dependency>
3646
```
3747

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

40-
Simply add the following to the body of our test class:
50+
Add the following to the body of our test class:
4151

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

4656
The `@Rule` annotation tells JUnit to notify this field about various events in the test lifecycle.
47-
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.
57+
Wrap the containers with `new TestContainersRule(...)` so the containers start and stop, according to the test lifecycle.
58+
In this case, our rule object is not `static`, so the container will start and stop with every test.
59+
The test configures `GenericContainer` to use a specific Redis image from Docker Hub, and to expose a port.
4860

4961
If we run our test as-is, then regardless of the actual test outcome, we'll see logs showing us that Testcontainers:
5062

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

6880
!!! tip
69-
Notice that we also ask Testcontainers for the container's actual address with `redis.getHost();`,
81+
Notice that we also ask Testcontainers for the container's actual address with `redis.getHost();`,
7082
rather than hard-coding `localhost`. `localhost` may work in some environments but not others - for example it may
71-
not work on your current or future CI environment. As such, **avoid hard-coding** the address, and use
83+
not work on your current or future CI environment. As such, **avoid hard-coding** the address, and use
7284
`getHost()` instead.
7385

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

modules/junit-vintage/build.gradle

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
description = "Testcontainers :: JUnit4 Rule"
2+
3+
dependencies {
4+
api project(":testcontainers")
5+
api 'junit:junit:4.13.2'
6+
implementation platform('org.junit:junit-bom:5.10.3')
7+
implementation 'org.junit.jupiter:junit-jupiter-api'
8+
9+
testImplementation 'com.datastax.oss:java-driver-core:4.17.0'
10+
testImplementation 'org.assertj:assertj-core:3.26.3'
11+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package org.testcontainers.junit.vintage;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
/**
9+
* The {@code @Container} annotation marks containers that should be managed by the
10+
* {@code Testcontainers} rule.
11+
*/
12+
@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE })
13+
@Retention(RetentionPolicy.RUNTIME)
14+
public @interface Container {
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package org.testcontainers.junit.vintage;
2+
3+
import org.junit.rules.TestRule;
4+
import org.junit.runner.Description;
5+
import org.junit.runners.model.MultipleFailureException;
6+
import org.junit.runners.model.Statement;
7+
import org.testcontainers.lifecycle.TestDescription;
8+
9+
import java.util.ArrayList;
10+
import java.util.List;
11+
12+
/**
13+
* {@link TestRule} which is called before and after each test, and also is notified on success/failure.
14+
*
15+
* This mimics the behaviour of TestWatcher to some degree, but failures occurring in this rule do not
16+
* contribute to the overall failure count (which can otherwise cause strange negative test success
17+
* figures).
18+
*/
19+
class FailureDetectingExternalResource implements TestRule {
20+
21+
@Override
22+
public final Statement apply(Statement base, Description description) {
23+
return new Statement() {
24+
@Override
25+
public void evaluate() throws Throwable {
26+
List<Throwable> errors = new ArrayList<Throwable>();
27+
28+
try {
29+
starting(description);
30+
base.evaluate();
31+
succeeded(description);
32+
} catch (Throwable e) {
33+
errors.add(e);
34+
failed(e, description);
35+
} finally {
36+
finished(description, errors);
37+
}
38+
39+
MultipleFailureException.assertEmpty(errors);
40+
}
41+
};
42+
}
43+
44+
protected void starting(Description description) throws Throwable {}
45+
46+
protected void succeeded(Description description) throws Throwable {}
47+
48+
protected void failed(Throwable e, Description description) {}
49+
50+
protected void finished(Description description, List<Throwable> errors) {}
51+
52+
protected static final TestDescription toTestDescription(Description description) {
53+
return new TestDescription() {
54+
@Override
55+
public String getTestId() {
56+
return description.getDisplayName();
57+
}
58+
59+
@Override
60+
public String getFilesystemFriendlyName() {
61+
return description.getClassName() + "-" + description.getMethodName();
62+
}
63+
};
64+
}
65+
}

0 commit comments

Comments
 (0)