Skip to content

Commit fa6fec8

Browse files
committed
Support applying @container to methods
1 parent 26171e4 commit fa6fec8

3 files changed

Lines changed: 73 additions & 36 deletions

File tree

modules/junit-vintage/src/main/java/org/testcontainers/junit/vintage/Container.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* The {@code @Container} annotation marks containers that should be managed by the
1010
* {@code Testcontainers} rule.
1111
*/
12-
@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE })
12+
@Target({ ElementType.FIELD, ElementType.METHOD })
1313
@Retention(RetentionPolicy.RUNTIME)
1414
public @interface Container {
1515
}

modules/junit-vintage/src/main/java/org/testcontainers/junit/vintage/Testcontainers.java

Lines changed: 67 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
package org.testcontainers.junit.vintage;
22

3+
import lombok.SneakyThrows;
34
import org.junit.platform.commons.support.AnnotationSupport;
45
import org.junit.platform.commons.support.HierarchyTraversalMode;
56
import org.junit.platform.commons.support.ModifierSupport;
67
import org.junit.platform.commons.support.ReflectionSupport;
78
import org.junit.runner.Description;
9+
import org.junit.runners.model.FrameworkMethod;
810
import org.junit.runners.model.MultipleFailureException;
911
import org.testcontainers.lifecycle.Startable;
1012
import org.testcontainers.lifecycle.TestDescription;
1113
import org.testcontainers.lifecycle.TestLifecycleAware;
1214

15+
import java.lang.reflect.AnnotatedElement;
1316
import java.lang.reflect.Field;
17+
import java.lang.reflect.Member;
18+
import java.lang.reflect.Method;
1419
import java.util.ArrayList;
1520
import java.util.Collections;
1621
import java.util.List;
@@ -20,12 +25,15 @@
2025
import java.util.function.Consumer;
2126
import java.util.function.Predicate;
2227
import java.util.stream.Collectors;
28+
import java.util.stream.Stream;
2329

2430
/**
2531
* Integrates Testcontainers with the JUnit4 lifecycle.
2632
*/
2733
public final class Testcontainers extends FailureDetectingExternalResource {
2834

35+
private static HierarchyTraversalMode TRAVERSAL_MODE = HierarchyTraversalMode.TOP_DOWN;
36+
2937
private final Object testInstance;
3038

3139
private List<Startable> startedContainers = Collections.emptyList();
@@ -116,51 +124,76 @@ protected void finished(Description description) throws Exception {
116124
}
117125

118126
private List<Startable> findContainers(Description description) {
119-
if (description.getTestClass() == null) {
127+
Class<?> testClass = description.getTestClass();
128+
if (testClass == null) {
120129
return Collections.emptyList();
121130
}
122-
Predicate<Field> isTargetedContainer = isContainer();
123-
if (testInstance == null) {
124-
isTargetedContainer = isTargetedContainer.and(ModifierSupport::isStatic);
125-
} else {
126-
isTargetedContainer = isTargetedContainer.and(ModifierSupport::isNotStatic);
127-
}
128131

129-
return ReflectionSupport
130-
.findFields(description.getTestClass(), isTargetedContainer, HierarchyTraversalMode.TOP_DOWN)
131-
.stream()
132-
.map(this::getContainerInstance)
132+
Predicate<Member> hasExpectedModifier = testInstance == null
133+
? ModifierSupport::isStatic
134+
: ModifierSupport::isNotStatic;
135+
136+
return Stream
137+
.of(
138+
ReflectionSupport
139+
.findMethods(testClass, isContainerMethod().and(hasExpectedModifier), TRAVERSAL_MODE)
140+
.stream()
141+
.map(this::createContainerInstance),
142+
ReflectionSupport
143+
.findFields(testClass, isContainerField().and(hasExpectedModifier), TRAVERSAL_MODE)
144+
.stream()
145+
.map(this::getContainerInstance)
146+
)
147+
.flatMap(s -> s)
133148
.collect(Collectors.toList());
134149
}
135150

136-
private static Predicate<Field> isContainer() {
137-
return field -> {
138-
boolean isAnnotatedWithContainer = AnnotationSupport.isAnnotated(field, Container.class);
139-
if (isAnnotatedWithContainer) {
140-
boolean isStartable = Startable.class.isAssignableFrom(field.getType());
151+
private static Predicate<Method> isContainerMethod() {
152+
return method -> isAnnotatedWithContainer(method);
153+
}
141154

142-
if (!isStartable) {
143-
throw new RuntimeException(
144-
String.format("The @Container field '%s' does not implement Startable", field.getName())
145-
);
146-
}
147-
return true;
148-
}
149-
return false;
150-
};
155+
private static Predicate<Field> isContainerField() {
156+
return field -> isAnnotatedWithContainer(field);
151157
}
152158

159+
private static boolean isAnnotatedWithContainer(AnnotatedElement element) {
160+
return AnnotationSupport.isAnnotated(element, Container.class);
161+
}
162+
163+
private Startable createContainerInstance(Method method) {
164+
if (!Startable.class.isAssignableFrom(method.getReturnType())) {
165+
throw new RuntimeException(
166+
String.format("The @Container method '%s()' does not return a Startable", method.getName())
167+
);
168+
}
169+
170+
method.setAccessible(true);
171+
Object container = invokeExplosively(method, testInstance);
172+
if (container == null) {
173+
throw new RuntimeException(String.format("The @Container method '%s()' returned null", method.getName()));
174+
}
175+
return (Startable) container;
176+
}
177+
178+
@SneakyThrows(IllegalAccessException.class)
153179
private Startable getContainerInstance(Field field) {
154-
try {
155-
field.setAccessible(true);
156-
Startable containerInstance = (Startable) field.get(testInstance);
157-
if (containerInstance == null) {
158-
throw new RuntimeException("Container " + field.getName() + " needs to be initialized");
159-
}
160-
return containerInstance;
161-
} catch (IllegalAccessException e) {
162-
throw new RuntimeException("Cannot access container defined in field " + field.getName());
180+
if (!Startable.class.isAssignableFrom(field.getType())) {
181+
throw new RuntimeException(
182+
String.format("The @Container field '%s' does not implement Startable", field.getName())
183+
);
163184
}
185+
186+
field.setAccessible(true);
187+
Startable container = (Startable) field.get(testInstance);
188+
if (container == null) {
189+
throw new RuntimeException("Container " + field.getName() + " needs to be initialized");
190+
}
191+
return container;
192+
}
193+
194+
@SneakyThrows(Throwable.class)
195+
private static Object invokeExplosively(Method method, Object target, final Object... params) {
196+
return new FrameworkMethod(method).invokeExplosively(target, params);
164197
}
165198

166199
private static <T> void forEachReversed(List<T> list, Consumer<? super T> callback) {

modules/junit-vintage/src/test/java/org/testcontainers/junit/vintage/TestcontainersTest.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,9 +171,13 @@ static void reset() {
171171
@Container
172172
private final TestLifecycleAwareContainerMock testContainer1 = new TestLifecycleAwareContainerMock();
173173

174-
@Container
175174
private final TestLifecycleAwareContainerMock testContainer2 = new TestLifecycleAwareContainerMock();
176175

176+
@Container
177+
private TestLifecycleAwareContainerMock createContainer() {
178+
return testContainer2;
179+
}
180+
177181
@Test
178182
public void containerStarted() {
179183
testsStarted++;

0 commit comments

Comments
 (0)