Skip to content
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

Support TestLifecycleAware-ness of containers started by the JUnit Jupiter integration #1326

Merged
merged 41 commits into from Apr 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
63764ca
Update BrowserWebDriverContainer to honor existing no_proxy setting
roamingthings Oct 21, 2018
3281ac5
Update test to only start one container per test
roamingthings Oct 22, 2018
631b9b3
Use constant for no_proxy key
roamingthings Oct 22, 2018
6c8f08c
Cleanup test implementation (#929)
roamingthings Feb 2, 2019
3143068
Merge branch 'master' into master
roamingthings Mar 7, 2019
4a1ffe0
Merge branch 'master' into master
roamingthings Mar 12, 2019
8c07732
Merge branch 'master' into master
rnorth Mar 17, 2019
a7ccf50
Merge branch 'master' into master
kiview Mar 19, 2019
7a9f69a
Merge remote-tracking branch 'remotes/upstream/master'
roamingthings Mar 19, 2019
c91d344
Add signalling of TestLifecycleAware containers.
roamingthings Feb 6, 2019
a6998eb
#1326 Add early draft to test signalling of TestLifecycleAware contai…
roamingthings Mar 19, 2019
e5a158d
Add test for post condition when signalling lifecycleaware containers
roamingthings Mar 27, 2019
a7fd8fd
Merge remote-tracking branch 'upstream/master'
roamingthings Jul 7, 2019
c374056
Merge branch 'master' into juni5_lifecycleaware
roamingthings Jul 7, 2019
a58a986
Merge remote 'upstream/master' into juni5_lifecycleaware
roamingthings Jul 21, 2019
ee1dae7
Update test for lifecycle aware containers to cover shared case (#1326)
roamingthings Jul 23, 2019
deabf8a
Merge remote-tracking branch 'upstream/master' into juni5_lifecycleaware
roamingthings Jul 23, 2019
63c723d
Update test for lifecycle aware containers to cover shared case (#1326)
roamingthings Jul 23, 2019
20eea51
Merge branch 'master' into juni5_lifecycleaware
roamingthings Sep 15, 2019
7971b3a
Merge branch 'master' into juni5_lifecycleaware
roamingthings Oct 18, 2019
61ac0c3
Merge branch 'master' into juni5_lifecycleaware
roamingthings Nov 1, 2019
93fb685
Fix formatting (#1326)
roamingthings Nov 1, 2019
13b526e
Use lighter container for testing (#1326)
roamingthings Nov 1, 2019
eb67ee2
Separate store and collect of shared lifecycle-aware-containers (#1326)
roamingthings Nov 1, 2019
37014cc
Add tests for ordering and capturing test exceptions (#1326)
roamingthings Nov 1, 2019
33b1e7b
Merge branch 'master' into juni5_lifecycleaware
roamingthings Dec 23, 2019
6013273
Make lifecycle tests independent of timing (#1326)
roamingthings Dec 23, 2019
5de31c8
Make mock now implements Startable (#1326)
roamingthings Dec 23, 2019
cef26a3
Merge branch 'master' into juni5_lifecycleaware
rnorth Jan 18, 2020
9abd4e6
Add AssertJ dependency (#1326)
roamingthings Jan 19, 2020
43ec440
Migrate assertions of TestLifecycleAwareMethodTest to AssertJ (#1326)
roamingthings Jan 19, 2020
856da3f
Update generation of filesystem friendly description (#1326)
roamingthings Jan 19, 2020
8689dda
Merge remote-tracking branch 'origin/juni5_lifecycleaware' into juni5…
roamingthings Jan 19, 2020
c93cebd
Separated tests for filesystem friendly filename (#1326)
roamingthings Jan 23, 2020
605d46a
Merge branch 'master' into juni5_lifecycleaware
roamingthings Feb 25, 2020
d25dc8c
Merge branch 'master' into juni5_lifecycleaware
roamingthings Feb 29, 2020
2fbdf85
Use lombok to improve readability (#1326)
roamingthings Feb 29, 2020
6f4f83a
Generate filesystem friendly name from display name (#1326)
roamingthings Feb 29, 2020
8297f42
Merge branch 'master' into juni5_lifecycleaware
roamingthings Apr 4, 2020
261e470
Generate filesystem friendly name from URLEncoded unique id (#1326)
roamingthings Apr 4, 2020
9690e50
Merge branch 'master' into juni5_lifecycleaware
roamingthings Apr 9, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions modules/junit-jupiter/build.gradle
Expand Up @@ -12,6 +12,8 @@ dependencies {
testCompile ('org.mockito:mockito-core:3.3.3') {
exclude(module: 'hamcrest-core')
}
testCompile 'org.junit.jupiter:junit-jupiter-params:5.6.0'
testCompile 'org.assertj:assertj-core:3.14.0'

testRuntime 'org.postgresql:postgresql:42.2.12'
testRuntime 'mysql:mysql-connector-java:8.0.19'
Expand Down
@@ -0,0 +1,24 @@
package org.testcontainers.junit.jupiter;

import org.junit.jupiter.api.extension.ExtensionContext;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.platform.commons.util.StringUtils.isBlank;

class FilesystemFriendlyNameGenerator {
private static final String UNKNOWN_NAME = "unknown";

static String filesystemFriendlyNameOf(ExtensionContext context) {
String contextId = context.getUniqueId();
try {
return (isBlank(contextId))
? UNKNOWN_NAME
: URLEncoder.encode(contextId, UTF_8.toString());
} catch (UnsupportedEncodingException e) {
return UNKNOWN_NAME;
}
}
}
@@ -0,0 +1,10 @@
package org.testcontainers.junit.jupiter;

import lombok.Value;
import org.testcontainers.lifecycle.TestDescription;

@Value
class TestcontainersTestDescription implements TestDescription {
String testId;
String filesystemFriendlyName;
}
@@ -1,32 +1,50 @@
package org.testcontainers.junit.jupiter;

import lombok.Getter;
import org.junit.jupiter.api.extension.*;
import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ConditionEvaluationResult;
import org.junit.jupiter.api.extension.ExecutionCondition;
import org.junit.jupiter.api.extension.ExtensionConfigurationException;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import org.junit.jupiter.api.extension.ExtensionContext.Store;
import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource;
import org.junit.jupiter.api.extension.TestInstancePostProcessor;
import org.junit.platform.commons.support.AnnotationSupport;
import org.junit.platform.commons.util.AnnotationUtils;
import org.junit.platform.commons.util.Preconditions;
import org.junit.platform.commons.util.ReflectionUtils;
import org.testcontainers.DockerClientFactory;
import org.testcontainers.lifecycle.Startable;
import org.testcontainers.lifecycle.TestDescription;
import org.testcontainers.lifecycle.TestLifecycleAware;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Stream;

class TestcontainersExtension implements BeforeEachCallback, BeforeAllCallback, ExecutionCondition, TestInstancePostProcessor {
import static java.util.stream.Collectors.toList;
import static org.testcontainers.junit.jupiter.FilesystemFriendlyNameGenerator.filesystemFriendlyNameOf;

class TestcontainersExtension implements BeforeEachCallback, BeforeAllCallback, AfterEachCallback, AfterAllCallback, ExecutionCondition, TestInstancePostProcessor {

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

private static final String TEST_INSTANCE = "testInstance";
private static final String SHARED_LIFECYCLE_AWARE_CONTAINERS = "sharedLifecycleAwareContainers";
private static final String LOCAL_LIFECYCLE_AWARE_CONTAINERS = "localLifecycleAwareContainers";

@Override
public void postProcessTestInstance(final Object testInstance, final ExtensionContext context) {
ExtensionContext.Store store = context.getStore(NAMESPACE);
Store store = context.getStore(NAMESPACE);
store.put(TEST_INSTANCE, testInstance);
}

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

ExtensionContext.Store store = context.getStore(NAMESPACE);
Store store = context.getStore(NAMESPACE);
List<StoreAdapter> sharedContainersStoreAdapters = findSharedContainers(testClass);

sharedContainersStoreAdapters.forEach(adapter -> store.getOrComputeIfAbsent(adapter.getKey(), k -> adapter.start()));

List<TestLifecycleAware> lifecycleAwareContainers = sharedContainersStoreAdapters
.stream()
.filter(this::isTestLifecycleAware)
.map(lifecycleAwareAdapter -> (TestLifecycleAware) lifecycleAwareAdapter.container)
.collect(toList());

findSharedContainers(testClass)
.forEach(adapter -> store.getOrComputeIfAbsent(adapter.getKey(), k -> adapter.start()));
store.put(SHARED_LIFECYCLE_AWARE_CONTAINERS, lifecycleAwareContainers);
signalBeforeTestToContainers(lifecycleAwareContainers, testDescriptionFrom(context));
}

@Override
public void afterAll(ExtensionContext context) {
signalAfterTestToContainersFor(SHARED_LIFECYCLE_AWARE_CONTAINERS, context);
}

@Override
public void beforeEach(final ExtensionContext context) {
collectParentTestInstances(context)
.parallelStream()
Store store = context.getStore(NAMESPACE);

List<TestLifecycleAware> lifecycleAwareContainers = collectParentTestInstances(context).parallelStream()
.flatMap(this::findRestartContainers)
.forEach(adapter -> context.getStore(NAMESPACE)
.getOrComputeIfAbsent(adapter.getKey(), k -> adapter.start()));
.peek(adapter -> store.getOrComputeIfAbsent(adapter.getKey(), k -> adapter.start()))
.filter(this::isTestLifecycleAware)
.map(lifecycleAwareAdapter -> (TestLifecycleAware) lifecycleAwareAdapter.container)
.collect(toList());

store.put(LOCAL_LIFECYCLE_AWARE_CONTAINERS, lifecycleAwareContainers);
signalBeforeTestToContainers(lifecycleAwareContainers, testDescriptionFrom(context));
}

@Override
public void afterEach(ExtensionContext context) {
signalAfterTestToContainersFor(LOCAL_LIFECYCLE_AWARE_CONTAINERS, context);
}

private void signalBeforeTestToContainers(List<TestLifecycleAware> lifecycleAwareContainers, TestDescription testDescription) {
lifecycleAwareContainers.forEach(container -> container.beforeTest(testDescription));
}

private void signalAfterTestToContainersFor(String storeKey, ExtensionContext context) {
List<TestLifecycleAware> lifecycleAwareContainers =
(List<TestLifecycleAware>) context.getStore(NAMESPACE).get(storeKey);
if (lifecycleAwareContainers != null) {
TestDescription description = testDescriptionFrom(context);
Optional<Throwable> throwable = context.getExecutionException();
lifecycleAwareContainers.forEach(container -> container.afterTest(description, throwable));
}
}

private TestDescription testDescriptionFrom(ExtensionContext context) {
return new TestcontainersTestDescription(
context.getUniqueId(),
filesystemFriendlyNameOf(context)
);
}

private boolean isTestLifecycleAware(StoreAdapter adapter) {
return adapter.container instanceof TestLifecycleAware;
}

@Override
Expand Down Expand Up @@ -101,13 +169,14 @@ private Set<Object> collectParentTestInstances(final ExtensionContext context) {
return testInstances;
}

private Stream<StoreAdapter> findSharedContainers(Class<?> testClass) {
private List<StoreAdapter> findSharedContainers(Class<?> testClass) {
return ReflectionUtils.findFields(
testClass,
isSharedContainer(),
ReflectionUtils.HierarchyTraversalMode.TOP_DOWN)
.stream()
.map(f -> getContainerInstance(null, f));
.map(f -> getContainerInstance(null, f))
.collect(toList());
}

private Predicate<Field> isSharedContainer() {
Expand Down
@@ -0,0 +1,45 @@
package org.testcontainers.junit.jupiter;

import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.stream.Stream;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.testcontainers.junit.jupiter.FilesystemFriendlyNameGenerator.filesystemFriendlyNameOf;

class FilesystemFriendlyNameGeneratorTest {

@ParameterizedTest
@MethodSource("provideDisplayNamesAndFilesystemFriendlyNames")
void should_generate_filesystem_friendly_name(String displayName, String expectedName) {
ExtensionContext context = mock(ExtensionContext.class);
doReturn(displayName)
.when(context).getUniqueId();

String filesystemFriendlyName = filesystemFriendlyNameOf(context);

assertThat(filesystemFriendlyName).isEqualTo(expectedName);
}

private static Stream<Arguments> provideDisplayNamesAndFilesystemFriendlyNames() {
return Stream.of(
Arguments.of("", "unknown"),
Arguments.of(" ", "unknown"),
Arguments.of("not blank", "not+blank"),
Arguments.of("abc ABC 1234567890", "abc+ABC+1234567890"),
Arguments.of(
"no_umlauts_äöüÄÖÜéáíó",
"no_umlauts_%C3%A4%C3%B6%C3%BC%C3%84%C3%96%C3%9C%C3%A9%C3%A1%C3%AD%C3%B3"
),
Arguments.of(
"[engine:junit-jupiter]/[class:com.example.MyTest]/[test-factory:parameterizedTest()]/[dynamic-test:#3]",
"%5Bengine%3Ajunit-jupiter%5D%2F%5Bclass%3Acom.example.MyTest%5D%2F%5Btest-factory%3AparameterizedTest%28%29%5D%2F%5Bdynamic-test%3A%233%5D"
)
);
}
}
@@ -0,0 +1,54 @@
package org.testcontainers.junit.jupiter;

import org.testcontainers.lifecycle.Startable;
import org.testcontainers.lifecycle.TestDescription;
import org.testcontainers.lifecycle.TestLifecycleAware;

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

public class TestLifecycleAwareContainerMock implements Startable, TestLifecycleAware {

static final String BEFORE_TEST = "beforeTest";
static final String AFTER_TEST = "afterTest";

private final List<String> lifecycleMethodCalls = new ArrayList<>();
private final List<String> lifecycleFilesystemFriendlyNames = new ArrayList<>();

private Throwable capturedThrowable;

@Override
public void beforeTest(TestDescription description) {
lifecycleMethodCalls.add(BEFORE_TEST);
lifecycleFilesystemFriendlyNames.add(description.getFilesystemFriendlyName());
}

@Override
public void afterTest(TestDescription description, Optional<Throwable> throwable) {
lifecycleMethodCalls.add(AFTER_TEST);
throwable.ifPresent(capturedThrowable -> this.capturedThrowable = capturedThrowable);
}

List<String> getLifecycleMethodCalls() {
return lifecycleMethodCalls;
}

Throwable getCapturedThrowable() {
return capturedThrowable;
}

public List<String> getLifecycleFilesystemFriendlyNames() {
return lifecycleFilesystemFriendlyNames;
}

@Override
public void start() {

}

@Override
public void stop() {

}
}
@@ -0,0 +1,37 @@
package org.testcontainers.junit.jupiter;

import org.junit.AssumptionViolatedException;
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;

// The order of @ExtendsWith and @Testcontainers is crucial in order for the tests
@Testcontainers
@TestMethodOrder(OrderAnnotation.class)
class TestLifecycleAwareExceptionCapturingTest {
@Container
private final TestLifecycleAwareContainerMock testContainer = new TestLifecycleAwareContainerMock();

private static TestLifecycleAwareContainerMock startedTestContainer;

@Test
@Order(1)
void failing_test_should_pass_throwable_to_testContainer() {
startedTestContainer = testContainer;
// Force an exception that is captured by the test container without failing the test itself
assumeTrue(false);
}

@Test
@Order(2)
void should_have_captured_thrownException() {
Throwable capturedThrowable = startedTestContainer.getCapturedThrowable();
assertTrue(capturedThrowable instanceof AssumptionViolatedException);
assertEquals("got: <false>, expected: is <true>", capturedThrowable.getMessage());
}
}