Skip to content

Commit

Permalink
Support TestLifecycleAware-ness of containers started by the JUnit Ju…
Browse files Browse the repository at this point in the history
…piter 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 <rich.north@gmail.com>
Co-authored-by: Kevin Wittek <kiview@users.noreply.github.com>
  • Loading branch information
3 people committed Apr 12, 2020
1 parent cbd3220 commit 84c8d1e
Show file tree
Hide file tree
Showing 8 changed files with 326 additions and 12 deletions.
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());
}
}

0 comments on commit 84c8d1e

Please sign in to comment.