Skip to content

Commit 5454ea9

Browse files
committed
Start shared containers in postProcessTestInstance for PER_CLASS lifecycle
With PER_CLASS, postProcessTestInstance runs before beforeAll. This change starts shared containers early so they are available in non-static @MethodSource factory methods and other TestInstancePostProcessor extensions. StoreAdapter now implements Startable with synchronized idempotent start/stop to support Startables.deepStart() and prevent redundant container lifecycle calls.
1 parent e38d920 commit 5454ea9

File tree

3 files changed

+195
-14
lines changed

3 files changed

+195
-14
lines changed

modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/TestcontainersExtension.java

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package org.testcontainers.junit.jupiter;
22

33
import lombok.Getter;
4+
import lombok.Synchronized;
5+
import org.junit.jupiter.api.TestInstance;
46
import org.junit.jupiter.api.extension.AfterAllCallback;
57
import org.junit.jupiter.api.extension.AfterEachCallback;
68
import org.junit.jupiter.api.extension.BeforeAllCallback;
@@ -12,6 +14,7 @@
1214
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
1315
import org.junit.jupiter.api.extension.ExtensionContext.Store;
1416
import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource;
17+
import org.junit.jupiter.api.extension.TestInstancePostProcessor;
1518
import org.junit.platform.commons.support.AnnotationSupport;
1619
import org.junit.platform.commons.support.HierarchyTraversalMode;
1720
import org.junit.platform.commons.support.ModifierSupport;
@@ -33,7 +36,13 @@
3336
import java.util.stream.Stream;
3437

3538
public class TestcontainersExtension
36-
implements BeforeEachCallback, BeforeAllCallback, AfterEachCallback, AfterAllCallback, ExecutionCondition {
39+
implements
40+
BeforeEachCallback,
41+
BeforeAllCallback,
42+
AfterEachCallback,
43+
AfterAllCallback,
44+
ExecutionCondition,
45+
TestInstancePostProcessor {
3746

3847
private static final Namespace NAMESPACE = Namespace.create(TestcontainersExtension.class);
3948

@@ -43,8 +52,23 @@ public class TestcontainersExtension
4352

4453
private final DockerAvailableDetector dockerDetector = new DockerAvailableDetector();
4554

55+
@Override
56+
public void postProcessTestInstance(Object testInstance, ExtensionContext context) {
57+
TestInstance.Lifecycle lifecycle = context.getTestInstanceLifecycle().orElse(null);
58+
if (lifecycle == TestInstance.Lifecycle.PER_CLASS) {
59+
beforeAllImpl(context);
60+
}
61+
}
62+
4663
@Override
4764
public void beforeAll(ExtensionContext context) {
65+
TestInstance.Lifecycle lifecycle = context.getTestInstanceLifecycle().orElse(null);
66+
if (lifecycle != TestInstance.Lifecycle.PER_CLASS) {
67+
beforeAllImpl(context);
68+
}
69+
}
70+
71+
private void beforeAllImpl(ExtensionContext context) {
4872
Class<?> testClass = context
4973
.getTestClass()
5074
.orElseThrow(() -> {
@@ -71,16 +95,14 @@ private void startContainers(List<StoreAdapter> storeAdapters, Store store, Exte
7195
return;
7296
}
7397

98+
List<StoreAdapter> storedAdapters = storeAdapters
99+
.stream()
100+
.map(adapter -> (StoreAdapter) store.getOrComputeIfAbsent(adapter.getKey(), k -> adapter))
101+
.collect(Collectors.toList());
74102
if (isParallelExecutionEnabled(context)) {
75-
Stream<Startable> startables = storeAdapters
76-
.stream()
77-
.map(storeAdapter -> {
78-
store.getOrComputeIfAbsent(storeAdapter.getKey(), k -> storeAdapter);
79-
return storeAdapter.container;
80-
});
81-
Startables.deepStart(startables).join();
103+
Startables.deepStart(storedAdapters).join();
82104
} else {
83-
storeAdapters.forEach(adapter -> store.getOrComputeIfAbsent(adapter.getKey(), k -> adapter.start()));
105+
storedAdapters.forEach(StoreAdapter::start);
84106
}
85107
}
86108

@@ -260,7 +282,7 @@ private static StoreAdapter getContainerInstance(final Object testInstance, fina
260282
* thereby letting the JUnit automatically stop containers once the current
261283
* {@link ExtensionContext} is closed.
262284
*/
263-
private static class StoreAdapter implements CloseableResource, AutoCloseable {
285+
private static class StoreAdapter implements Startable, CloseableResource, AutoCloseable {
264286

265287
@Getter
266288
private String key;
@@ -272,14 +294,29 @@ private StoreAdapter(Class<?> declaringClass, String fieldName, Startable contai
272294
this.container = container;
273295
}
274296

275-
private StoreAdapter start() {
276-
container.start();
277-
return this;
297+
private boolean started;
298+
299+
@Override
300+
@Synchronized
301+
public void start() {
302+
if (!started) {
303+
container.start();
304+
started = true;
305+
}
306+
}
307+
308+
@Override
309+
@Synchronized
310+
public void stop() {
311+
if (started) {
312+
container.stop();
313+
started = false;
314+
}
278315
}
279316

280317
@Override
281318
public void close() {
282-
container.stop();
319+
stop();
283320
}
284321
}
285322
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package org.testcontainers.junit.jupiter;
2+
3+
import org.junit.jupiter.api.MethodOrderer;
4+
import org.junit.jupiter.api.Nested;
5+
import org.junit.jupiter.api.Order;
6+
import org.junit.jupiter.api.Test;
7+
import org.junit.jupiter.api.TestInstance;
8+
import org.junit.jupiter.api.TestMethodOrder;
9+
import org.testcontainers.lifecycle.Startable;
10+
11+
import static org.assertj.core.api.Assertions.assertThat;
12+
13+
/**
14+
* Verifies that instance {@link Container @Container} fields are started exactly once
15+
* per test instance for both {@link TestInstance.Lifecycle#PER_CLASS} and
16+
* {@link TestInstance.Lifecycle#PER_METHOD} lifecycles.
17+
*/
18+
@Testcontainers
19+
class TestcontainersInstanceLifecycleTest {
20+
21+
@Container
22+
private static final StartCountingMock staticContainer = new StartCountingMock();
23+
24+
@Nested
25+
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
26+
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
27+
class PerClass {
28+
29+
@Container
30+
private final StartCountingMock instanceContainer = new StartCountingMock();
31+
32+
@Test
33+
@Order(1)
34+
void first_test() {
35+
assertThat(staticContainer.starts).isEqualTo(1);
36+
assertThat(instanceContainer.starts).isEqualTo(1);
37+
}
38+
39+
@Test
40+
@Order(2)
41+
void second_test() {
42+
assertThat(staticContainer.starts).as("Static container should be started exactly once").isEqualTo(1);
43+
assertThat(instanceContainer.starts)
44+
.as("PER_CLASS instance container should be started for every test")
45+
.isEqualTo(2);
46+
}
47+
}
48+
49+
@Nested
50+
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
51+
class PerMethod {
52+
53+
@Container
54+
private final StartCountingMock instanceContainer = new StartCountingMock();
55+
56+
@Test
57+
@Order(1)
58+
void first_test() {
59+
assertThat(staticContainer.starts).isEqualTo(1);
60+
assertThat(instanceContainer.starts).isEqualTo(1);
61+
}
62+
63+
@Test
64+
@Order(2)
65+
void second_test() {
66+
assertThat(staticContainer.starts).as("Static container should be started exactly once").isEqualTo(1);
67+
assertThat(instanceContainer.starts).isEqualTo(1);
68+
}
69+
}
70+
71+
static class StartCountingMock implements Startable {
72+
73+
int starts;
74+
75+
@Override
76+
public void start() {
77+
starts++;
78+
}
79+
80+
@Override
81+
public void stop() {}
82+
}
83+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package org.testcontainers.junit.jupiter;
2+
3+
import org.junit.jupiter.api.TestInstance;
4+
import org.junit.jupiter.params.ParameterizedTest;
5+
import org.junit.jupiter.params.provider.MethodSource;
6+
import org.testcontainers.lifecycle.Startable;
7+
8+
import java.util.UUID;
9+
import java.util.stream.Stream;
10+
11+
import static org.assertj.core.api.Assertions.assertThat;
12+
13+
/**
14+
* Verifies that static {@link Container @Container} fields are available in non-static
15+
* {@link MethodSource @MethodSource} factory methods with
16+
* {@link TestInstance.Lifecycle#PER_CLASS PER_CLASS} lifecycle.
17+
*/
18+
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
19+
@Testcontainers
20+
class TestcontainersPerClassPostProcessorTest {
21+
22+
@Container
23+
private static final StartTrackingMock staticContainer = new StartTrackingMock();
24+
25+
@Container
26+
private final StartTrackingMock instanceContainer = new StartTrackingMock();
27+
28+
private boolean staticStartedDuringMethodSource;
29+
30+
private boolean instanceStartedDuringMethodSource;
31+
32+
Stream<String> arguments() {
33+
staticStartedDuringMethodSource = staticContainer.containerId != null;
34+
instanceStartedDuringMethodSource = instanceContainer.containerId != null;
35+
return Stream.of("a");
36+
}
37+
38+
@ParameterizedTest
39+
@MethodSource("arguments")
40+
void containers_are_started_before_method_source(String argument) {
41+
assertThat(staticStartedDuringMethodSource)
42+
.as("Static container should be started before @MethodSource resolution")
43+
.isTrue();
44+
assertThat(instanceStartedDuringMethodSource)
45+
.as("Instance container should NOT be started before @MethodSource resolution")
46+
.isFalse();
47+
}
48+
49+
static class StartTrackingMock implements Startable {
50+
51+
String containerId;
52+
53+
@Override
54+
public void start() {
55+
containerId = UUID.randomUUID().toString();
56+
}
57+
58+
@Override
59+
public void stop() {}
60+
}
61+
}

0 commit comments

Comments
 (0)