Skip to content

Commit 84c8d1e

Browse files
roamingthingsrnorthkiview
authored
Support TestLifecycleAware-ness of containers started by the JUnit Jupiter integration (#1326)
* Update BrowserWebDriverContainer to honor existing no_proxy setting * Update test to only start one container per test * Use constant for no_proxy key * Cleanup test implementation (#929) * Add signalling of TestLifecycleAware containers. Allow containers like WebBrowserContainers to initialize and/or finalize before/after tests. * #1326 Add early draft to test signalling of TestLifecycleAware containers * Add test for post condition when signalling lifecycleaware containers This kind of test is a bit tricky since the post condition occurs after the original test has been finished. Also it's not nice to pass data between two tests. * Update test for lifecycle aware containers to cover shared case (#1326) In order to check that the afterAll callback has signalled lifecycleaware containers correctly a second extension is used. The order of the extension annotation ensures that the assertion is run after the extension under test. * Update test for lifecycle aware containers to cover shared case (#1326) To test the beforeAll() case the assertion has to be called from within the test class since it's called after all extensions. * Fix formatting (#1326) * Use lighter container for testing (#1326) * Separate store and collect of shared lifecycle-aware-containers (#1326) * Add tests for ordering and capturing test exceptions (#1326) * Make lifecycle tests independent of timing (#1326) Calls to the lifecycle methods are now recorded in an ordered list that is then used to test the correct number and order of calls. This makes the test independent of timing. Unfortunately it's still required to execute tests in a deterministic order. For a better separation of test concerns tests for the lifecycle methods and exception capturing have been moved into separate test classes. * Make mock now implements Startable (#1326) There is no need to start a container since only the TestLifecycleAware is important. * Add AssertJ dependency (#1326) We want to use AssertJ for some tests. * Migrate assertions of TestLifecycleAwareMethodTest to AssertJ (#1326) * Update generation of filesystem friendly description (#1326) * Separated tests for filesystem friendly filename (#1326) * Use lombok to improve readability (#1326) * Generate filesystem friendly name from display name (#1326) Generating a filesystem friendly name in a Junit Jupiter test is a bit tricky. Since tests can be generated dynamically class and/or test method may not be available. The display name provided by the ExtensionContext on the other hand may use characters that are not filesystem safe. This approach removes all characters from the display name that are not in a restricted set of allowed characters. However this may lead to name clashes if two tests have a display name that only differs in characters that are removed from the display name. * Generate filesystem friendly name from URLEncoded unique id (#1326) Co-authored-by: Richard North <[email protected]> Co-authored-by: Kevin Wittek <[email protected]>
1 parent cbd3220 commit 84c8d1e

File tree

8 files changed

+326
-12
lines changed

8 files changed

+326
-12
lines changed

modules/junit-jupiter/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ dependencies {
1212
testCompile ('org.mockito:mockito-core:3.3.3') {
1313
exclude(module: 'hamcrest-core')
1414
}
15+
testCompile 'org.junit.jupiter:junit-jupiter-params:5.6.0'
16+
testCompile 'org.assertj:assertj-core:3.14.0'
1517

1618
testRuntime 'org.postgresql:postgresql:42.2.12'
1719
testRuntime 'mysql:mysql-connector-java:8.0.19'
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package org.testcontainers.junit.jupiter;
2+
3+
import org.junit.jupiter.api.extension.ExtensionContext;
4+
5+
import java.io.UnsupportedEncodingException;
6+
import java.net.URLEncoder;
7+
8+
import static java.nio.charset.StandardCharsets.UTF_8;
9+
import static org.junit.platform.commons.util.StringUtils.isBlank;
10+
11+
class FilesystemFriendlyNameGenerator {
12+
private static final String UNKNOWN_NAME = "unknown";
13+
14+
static String filesystemFriendlyNameOf(ExtensionContext context) {
15+
String contextId = context.getUniqueId();
16+
try {
17+
return (isBlank(contextId))
18+
? UNKNOWN_NAME
19+
: URLEncoder.encode(contextId, UTF_8.toString());
20+
} catch (UnsupportedEncodingException e) {
21+
return UNKNOWN_NAME;
22+
}
23+
}
24+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package org.testcontainers.junit.jupiter;
2+
3+
import lombok.Value;
4+
import org.testcontainers.lifecycle.TestDescription;
5+
6+
@Value
7+
class TestcontainersTestDescription implements TestDescription {
8+
String testId;
9+
String filesystemFriendlyName;
10+
}

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

Lines changed: 81 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,50 @@
11
package org.testcontainers.junit.jupiter;
22

33
import lombok.Getter;
4-
import org.junit.jupiter.api.extension.*;
4+
import org.junit.jupiter.api.extension.AfterAllCallback;
5+
import org.junit.jupiter.api.extension.AfterEachCallback;
6+
import org.junit.jupiter.api.extension.BeforeAllCallback;
7+
import org.junit.jupiter.api.extension.BeforeEachCallback;
8+
import org.junit.jupiter.api.extension.ConditionEvaluationResult;
9+
import org.junit.jupiter.api.extension.ExecutionCondition;
10+
import org.junit.jupiter.api.extension.ExtensionConfigurationException;
11+
import org.junit.jupiter.api.extension.ExtensionContext;
512
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
13+
import org.junit.jupiter.api.extension.ExtensionContext.Store;
614
import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource;
15+
import org.junit.jupiter.api.extension.TestInstancePostProcessor;
716
import org.junit.platform.commons.support.AnnotationSupport;
817
import org.junit.platform.commons.util.AnnotationUtils;
918
import org.junit.platform.commons.util.Preconditions;
1019
import org.junit.platform.commons.util.ReflectionUtils;
1120
import org.testcontainers.DockerClientFactory;
1221
import org.testcontainers.lifecycle.Startable;
22+
import org.testcontainers.lifecycle.TestDescription;
23+
import org.testcontainers.lifecycle.TestLifecycleAware;
1324

1425
import java.lang.reflect.Field;
26+
import java.lang.reflect.Method;
1527
import java.util.LinkedHashSet;
28+
import java.util.List;
1629
import java.util.Optional;
1730
import java.util.Set;
1831
import java.util.function.Predicate;
1932
import java.util.stream.Stream;
2033

21-
class TestcontainersExtension implements BeforeEachCallback, BeforeAllCallback, ExecutionCondition, TestInstancePostProcessor {
34+
import static java.util.stream.Collectors.toList;
35+
import static org.testcontainers.junit.jupiter.FilesystemFriendlyNameGenerator.filesystemFriendlyNameOf;
36+
37+
class TestcontainersExtension implements BeforeEachCallback, BeforeAllCallback, AfterEachCallback, AfterAllCallback, ExecutionCondition, TestInstancePostProcessor {
2238

2339
private static final Namespace NAMESPACE = Namespace.create(TestcontainersExtension.class);
2440

2541
private static final String TEST_INSTANCE = "testInstance";
42+
private static final String SHARED_LIFECYCLE_AWARE_CONTAINERS = "sharedLifecycleAwareContainers";
43+
private static final String LOCAL_LIFECYCLE_AWARE_CONTAINERS = "localLifecycleAwareContainers";
2644

2745
@Override
2846
public void postProcessTestInstance(final Object testInstance, final ExtensionContext context) {
29-
ExtensionContext.Store store = context.getStore(NAMESPACE);
47+
Store store = context.getStore(NAMESPACE);
3048
store.put(TEST_INSTANCE, testInstance);
3149
}
3250

@@ -35,19 +53,69 @@ public void beforeAll(ExtensionContext context) {
3553
Class<?> testClass = context.getTestClass()
3654
.orElseThrow(() -> new ExtensionConfigurationException("TestcontainersExtension is only supported for classes."));
3755

38-
ExtensionContext.Store store = context.getStore(NAMESPACE);
56+
Store store = context.getStore(NAMESPACE);
57+
List<StoreAdapter> sharedContainersStoreAdapters = findSharedContainers(testClass);
58+
59+
sharedContainersStoreAdapters.forEach(adapter -> store.getOrComputeIfAbsent(adapter.getKey(), k -> adapter.start()));
60+
61+
List<TestLifecycleAware> lifecycleAwareContainers = sharedContainersStoreAdapters
62+
.stream()
63+
.filter(this::isTestLifecycleAware)
64+
.map(lifecycleAwareAdapter -> (TestLifecycleAware) lifecycleAwareAdapter.container)
65+
.collect(toList());
3966

40-
findSharedContainers(testClass)
41-
.forEach(adapter -> store.getOrComputeIfAbsent(adapter.getKey(), k -> adapter.start()));
67+
store.put(SHARED_LIFECYCLE_AWARE_CONTAINERS, lifecycleAwareContainers);
68+
signalBeforeTestToContainers(lifecycleAwareContainers, testDescriptionFrom(context));
69+
}
70+
71+
@Override
72+
public void afterAll(ExtensionContext context) {
73+
signalAfterTestToContainersFor(SHARED_LIFECYCLE_AWARE_CONTAINERS, context);
4274
}
4375

4476
@Override
4577
public void beforeEach(final ExtensionContext context) {
46-
collectParentTestInstances(context)
47-
.parallelStream()
78+
Store store = context.getStore(NAMESPACE);
79+
80+
List<TestLifecycleAware> lifecycleAwareContainers = collectParentTestInstances(context).parallelStream()
4881
.flatMap(this::findRestartContainers)
49-
.forEach(adapter -> context.getStore(NAMESPACE)
50-
.getOrComputeIfAbsent(adapter.getKey(), k -> adapter.start()));
82+
.peek(adapter -> store.getOrComputeIfAbsent(adapter.getKey(), k -> adapter.start()))
83+
.filter(this::isTestLifecycleAware)
84+
.map(lifecycleAwareAdapter -> (TestLifecycleAware) lifecycleAwareAdapter.container)
85+
.collect(toList());
86+
87+
store.put(LOCAL_LIFECYCLE_AWARE_CONTAINERS, lifecycleAwareContainers);
88+
signalBeforeTestToContainers(lifecycleAwareContainers, testDescriptionFrom(context));
89+
}
90+
91+
@Override
92+
public void afterEach(ExtensionContext context) {
93+
signalAfterTestToContainersFor(LOCAL_LIFECYCLE_AWARE_CONTAINERS, context);
94+
}
95+
96+
private void signalBeforeTestToContainers(List<TestLifecycleAware> lifecycleAwareContainers, TestDescription testDescription) {
97+
lifecycleAwareContainers.forEach(container -> container.beforeTest(testDescription));
98+
}
99+
100+
private void signalAfterTestToContainersFor(String storeKey, ExtensionContext context) {
101+
List<TestLifecycleAware> lifecycleAwareContainers =
102+
(List<TestLifecycleAware>) context.getStore(NAMESPACE).get(storeKey);
103+
if (lifecycleAwareContainers != null) {
104+
TestDescription description = testDescriptionFrom(context);
105+
Optional<Throwable> throwable = context.getExecutionException();
106+
lifecycleAwareContainers.forEach(container -> container.afterTest(description, throwable));
107+
}
108+
}
109+
110+
private TestDescription testDescriptionFrom(ExtensionContext context) {
111+
return new TestcontainersTestDescription(
112+
context.getUniqueId(),
113+
filesystemFriendlyNameOf(context)
114+
);
115+
}
116+
117+
private boolean isTestLifecycleAware(StoreAdapter adapter) {
118+
return adapter.container instanceof TestLifecycleAware;
51119
}
52120

53121
@Override
@@ -101,13 +169,14 @@ private Set<Object> collectParentTestInstances(final ExtensionContext context) {
101169
return testInstances;
102170
}
103171

104-
private Stream<StoreAdapter> findSharedContainers(Class<?> testClass) {
172+
private List<StoreAdapter> findSharedContainers(Class<?> testClass) {
105173
return ReflectionUtils.findFields(
106174
testClass,
107175
isSharedContainer(),
108176
ReflectionUtils.HierarchyTraversalMode.TOP_DOWN)
109177
.stream()
110-
.map(f -> getContainerInstance(null, f));
178+
.map(f -> getContainerInstance(null, f))
179+
.collect(toList());
111180
}
112181

113182
private Predicate<Field> isSharedContainer() {
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package org.testcontainers.junit.jupiter;
2+
3+
import org.junit.jupiter.api.extension.ExtensionContext;
4+
import org.junit.jupiter.params.ParameterizedTest;
5+
import org.junit.jupiter.params.provider.Arguments;
6+
import org.junit.jupiter.params.provider.MethodSource;
7+
8+
import java.util.stream.Stream;
9+
10+
import static org.assertj.core.api.Assertions.assertThat;
11+
import static org.mockito.Mockito.doReturn;
12+
import static org.mockito.Mockito.mock;
13+
import static org.testcontainers.junit.jupiter.FilesystemFriendlyNameGenerator.filesystemFriendlyNameOf;
14+
15+
class FilesystemFriendlyNameGeneratorTest {
16+
17+
@ParameterizedTest
18+
@MethodSource("provideDisplayNamesAndFilesystemFriendlyNames")
19+
void should_generate_filesystem_friendly_name(String displayName, String expectedName) {
20+
ExtensionContext context = mock(ExtensionContext.class);
21+
doReturn(displayName)
22+
.when(context).getUniqueId();
23+
24+
String filesystemFriendlyName = filesystemFriendlyNameOf(context);
25+
26+
assertThat(filesystemFriendlyName).isEqualTo(expectedName);
27+
}
28+
29+
private static Stream<Arguments> provideDisplayNamesAndFilesystemFriendlyNames() {
30+
return Stream.of(
31+
Arguments.of("", "unknown"),
32+
Arguments.of(" ", "unknown"),
33+
Arguments.of("not blank", "not+blank"),
34+
Arguments.of("abc ABC 1234567890", "abc+ABC+1234567890"),
35+
Arguments.of(
36+
"no_umlauts_äöüÄÖÜéáíó",
37+
"no_umlauts_%C3%A4%C3%B6%C3%BC%C3%84%C3%96%C3%9C%C3%A9%C3%A1%C3%AD%C3%B3"
38+
),
39+
Arguments.of(
40+
"[engine:junit-jupiter]/[class:com.example.MyTest]/[test-factory:parameterizedTest()]/[dynamic-test:#3]",
41+
"%5Bengine%3Ajunit-jupiter%5D%2F%5Bclass%3Acom.example.MyTest%5D%2F%5Btest-factory%3AparameterizedTest%28%29%5D%2F%5Bdynamic-test%3A%233%5D"
42+
)
43+
);
44+
}
45+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package org.testcontainers.junit.jupiter;
2+
3+
import org.testcontainers.lifecycle.Startable;
4+
import org.testcontainers.lifecycle.TestDescription;
5+
import org.testcontainers.lifecycle.TestLifecycleAware;
6+
7+
import java.util.ArrayList;
8+
import java.util.List;
9+
import java.util.Optional;
10+
11+
public class TestLifecycleAwareContainerMock implements Startable, TestLifecycleAware {
12+
13+
static final String BEFORE_TEST = "beforeTest";
14+
static final String AFTER_TEST = "afterTest";
15+
16+
private final List<String> lifecycleMethodCalls = new ArrayList<>();
17+
private final List<String> lifecycleFilesystemFriendlyNames = new ArrayList<>();
18+
19+
private Throwable capturedThrowable;
20+
21+
@Override
22+
public void beforeTest(TestDescription description) {
23+
lifecycleMethodCalls.add(BEFORE_TEST);
24+
lifecycleFilesystemFriendlyNames.add(description.getFilesystemFriendlyName());
25+
}
26+
27+
@Override
28+
public void afterTest(TestDescription description, Optional<Throwable> throwable) {
29+
lifecycleMethodCalls.add(AFTER_TEST);
30+
throwable.ifPresent(capturedThrowable -> this.capturedThrowable = capturedThrowable);
31+
}
32+
33+
List<String> getLifecycleMethodCalls() {
34+
return lifecycleMethodCalls;
35+
}
36+
37+
Throwable getCapturedThrowable() {
38+
return capturedThrowable;
39+
}
40+
41+
public List<String> getLifecycleFilesystemFriendlyNames() {
42+
return lifecycleFilesystemFriendlyNames;
43+
}
44+
45+
@Override
46+
public void start() {
47+
48+
}
49+
50+
@Override
51+
public void stop() {
52+
53+
}
54+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package org.testcontainers.junit.jupiter;
2+
3+
import org.junit.AssumptionViolatedException;
4+
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
5+
import org.junit.jupiter.api.Order;
6+
import org.junit.jupiter.api.Test;
7+
import org.junit.jupiter.api.TestMethodOrder;
8+
9+
import static org.junit.Assert.assertEquals;
10+
import static org.junit.Assert.assertTrue;
11+
import static org.junit.Assume.assumeTrue;
12+
13+
// The order of @ExtendsWith and @Testcontainers is crucial in order for the tests
14+
@Testcontainers
15+
@TestMethodOrder(OrderAnnotation.class)
16+
class TestLifecycleAwareExceptionCapturingTest {
17+
@Container
18+
private final TestLifecycleAwareContainerMock testContainer = new TestLifecycleAwareContainerMock();
19+
20+
private static TestLifecycleAwareContainerMock startedTestContainer;
21+
22+
@Test
23+
@Order(1)
24+
void failing_test_should_pass_throwable_to_testContainer() {
25+
startedTestContainer = testContainer;
26+
// Force an exception that is captured by the test container without failing the test itself
27+
assumeTrue(false);
28+
}
29+
30+
@Test
31+
@Order(2)
32+
void should_have_captured_thrownException() {
33+
Throwable capturedThrowable = startedTestContainer.getCapturedThrowable();
34+
assertTrue(capturedThrowable instanceof AssumptionViolatedException);
35+
assertEquals("got: <false>, expected: is <true>", capturedThrowable.getMessage());
36+
}
37+
}

0 commit comments

Comments
 (0)