From d1beb9fac3d69585d0a3761b9e189339f74949d6 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sun, 1 Aug 2021 15:33:40 +0200 Subject: [PATCH 01/12] Support extension registration via constructor & method params Issue: #864 --- .../descriptor/ClassBasedTestDescriptor.java | 20 +- .../engine/descriptor/ExtensionUtils.java | 44 +- .../descriptor/TestMethodTestDescriptor.java | 6 +- .../execution/DefaultParameterContext.java | 56 +- .../engine/extension/ExtensionRegistrar.java | 12 + .../extension/MutableExtensionRegistry.java | 12 +- ...gistrationViaParametersAndFieldsTests.java | 551 ++++++++++++++++++ .../commons/util/AnnotationUtils.java | 77 +++ 8 files changed, 709 insertions(+), 69 deletions(-) create mode 100644 junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java index da6f15e851c..cd71b2b9d5e 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java @@ -13,6 +13,8 @@ import static java.util.stream.Collectors.joining; import static org.apiguardian.api.API.Status.INTERNAL; import static org.junit.jupiter.engine.descriptor.ExtensionUtils.populateNewExtensionRegistryFromExtendWithAnnotation; +import static org.junit.jupiter.engine.descriptor.ExtensionUtils.registerExtensionsFromConstructorParameters; +import static org.junit.jupiter.engine.descriptor.ExtensionUtils.registerExtensionsFromExecutableParameters; import static org.junit.jupiter.engine.descriptor.ExtensionUtils.registerExtensionsFromFields; import static org.junit.jupiter.engine.descriptor.LifecycleMethodUtils.findAfterAllMethods; import static org.junit.jupiter.engine.descriptor.LifecycleMethodUtils.findAfterEachMethods; @@ -152,6 +154,16 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte // one factory registered per class). this.testInstanceFactory = resolveTestInstanceFactory(registry); + if (this.testInstanceFactory == null) { + registerExtensionsFromConstructorParameters(registry, this.testClass); + } + + this.beforeAllMethods = findBeforeAllMethods(this.testClass, this.lifecycle == Lifecycle.PER_METHOD); + this.afterAllMethods = findAfterAllMethods(this.testClass, this.lifecycle == Lifecycle.PER_METHOD); + + this.beforeAllMethods.forEach(method -> registerExtensionsFromExecutableParameters(registry, method)); + this.afterAllMethods.forEach(method -> registerExtensionsFromExecutableParameters(registry, method)); + registerBeforeEachMethodAdapters(registry); registerAfterEachMethodAdapters(registry); @@ -159,9 +171,6 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte ClassExtensionContext extensionContext = new ClassExtensionContext(context.getExtensionContext(), context.getExecutionListener(), this, this.lifecycle, context.getConfiguration(), throwableCollector); - this.beforeAllMethods = findBeforeAllMethods(this.testClass, this.lifecycle == Lifecycle.PER_METHOD); - this.afterAllMethods = findAfterAllMethods(this.testClass, this.lifecycle == Lifecycle.PER_METHOD); - // @formatter:off return context.extend() .withTestInstancesProvider(testInstancesProvider(context, extensionContext)) @@ -468,7 +477,10 @@ private void registerAfterEachMethodAdapters(ExtensionRegistrar registrar) { private void registerMethodsAsExtensions(List methods, ExtensionRegistrar registrar, Function extensionSynthesizer) { - methods.forEach(method -> registrar.registerSyntheticExtension(extensionSynthesizer.apply(method), method)); + methods.forEach(method -> { + registerExtensionsFromExecutableParameters(registrar, method); + registrar.registerSyntheticExtension(extensionSynthesizer.apply(method), method); + }); } private BeforeEachMethodAdapter synthesizeBeforeEachMethodAdapter(Method method) { diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java index 0c1ed17525e..951a3cb4afb 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java @@ -14,15 +14,19 @@ import static org.junit.platform.commons.util.AnnotationUtils.findAnnotatedFields; import static org.junit.platform.commons.util.AnnotationUtils.findAnnotation; import static org.junit.platform.commons.util.AnnotationUtils.findRepeatableAnnotations; +import static org.junit.platform.commons.util.ReflectionUtils.getDeclaredConstructor; import static org.junit.platform.commons.util.ReflectionUtils.isNotPrivate; import static org.junit.platform.commons.util.ReflectionUtils.tryToReadFieldValue; import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Executable; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Comparator; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Predicate; import org.junit.jupiter.api.Order; @@ -78,7 +82,7 @@ static MutableExtensionRegistry populateNewExtensionRegistryFromExtendWithAnnota } /** - * Register extensions in the supplied registry from fields in the supplied + * Register extensions using the supplied registrar from fields in the supplied * class that are annotated with {@link RegisterExtension @RegisterExtension}. * *

The extensions will be sorted according to {@link Order @Order} semantics @@ -115,6 +119,44 @@ static void registerExtensionsFromFields(ExtensionRegistrar registrar, Class }); } + /** + * Register extensions using the supplied registrar from parameters in the + * declared constructor of the supplied class that are annotated with + * {@link ExtendWith @ExtendWith}. + * + * @param registrar the registrar with which to register the extensions; never {@code null} + * @param clazz the class in which to find the declared constructor; never {@code null} + * @since 5.8 + */ + static void registerExtensionsFromConstructorParameters(ExtensionRegistrar registrar, Class clazz) { + registerExtensionsFromExecutableParameters(registrar, getDeclaredConstructor(clazz)); + } + + /** + * Register extensions using the supplied registrar from parameters in the + * supplied {@link Executable} (i.e., a {@link java.lang.reflect.Constructor} + * or {@link java.lang.reflect.Method}) that are annotated with{@link ExtendWith @ExtendWith}. + * + * @param registrar the registrar with which to register the extensions; never {@code null} + * @param executable the constructor or method whose parameters should be searched; never {@code null} + * @since 5.8 + */ + static void registerExtensionsFromExecutableParameters(ExtensionRegistrar registrar, Executable executable) { + Preconditions.notNull(registrar, "ExtensionRegistrar must not be null"); + Preconditions.notNull(executable, "Executable must not be null"); + + AtomicInteger index = new AtomicInteger(); + + // @formatter:off + Arrays.stream(executable.getParameters()) + .map(parameter -> findRepeatableAnnotations(parameter, index.getAndIncrement(), ExtendWith.class)) + .flatMap(Collection::stream) + .map(ExtendWith::value) + .flatMap(Arrays::stream) + .forEach(registrar::registerExtension); + // @formatter:on + } + /** * @since 5.4 */ diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java index ced91788031..a6bd1aea309 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java @@ -12,6 +12,7 @@ import static org.apiguardian.api.API.Status.INTERNAL; import static org.junit.jupiter.engine.descriptor.ExtensionUtils.populateNewExtensionRegistryFromExtendWithAnnotation; +import static org.junit.jupiter.engine.descriptor.ExtensionUtils.registerExtensionsFromExecutableParameters; import static org.junit.jupiter.engine.support.JupiterThrowableCollectorFactory.createThrowableCollector; import java.lang.reflect.Method; @@ -113,7 +114,10 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte } protected MutableExtensionRegistry populateNewExtensionRegistry(JupiterEngineExecutionContext context) { - return populateNewExtensionRegistryFromExtendWithAnnotation(context.getExtensionRegistry(), getTestMethod()); + MutableExtensionRegistry registry = populateNewExtensionRegistryFromExtendWithAnnotation( + context.getExtensionRegistry(), getTestMethod()); + registerExtensionsFromExecutableParameters(registry, getTestMethod()); + return registry; } @Override diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/DefaultParameterContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/DefaultParameterContext.java index 9ee48c40ae0..4a2d81cad10 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/DefaultParameterContext.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/DefaultParameterContext.java @@ -10,12 +10,7 @@ package org.junit.jupiter.engine.execution; -import static org.junit.platform.commons.util.ReflectionUtils.isInnerClass; - import java.lang.annotation.Annotation; -import java.lang.reflect.AnnotatedElement; -import java.lang.reflect.Constructor; -import java.lang.reflect.Executable; import java.lang.reflect.Parameter; import java.util.List; import java.util.Optional; @@ -58,62 +53,17 @@ public Optional getTarget() { @Override public boolean isAnnotated(Class annotationType) { - return AnnotationUtils.isAnnotated(getEffectiveAnnotatedParameter(), annotationType); + return AnnotationUtils.isAnnotated(this.parameter, this.index, annotationType); } @Override public Optional findAnnotation(Class annotationType) { - return AnnotationUtils.findAnnotation(getEffectiveAnnotatedParameter(), annotationType); + return AnnotationUtils.findAnnotation(this.parameter, this.index, annotationType); } @Override public List findRepeatableAnnotations(Class annotationType) { - return AnnotationUtils.findRepeatableAnnotations(getEffectiveAnnotatedParameter(), annotationType); - } - - /** - * Due to a bug in {@code javac} on JDK versions prior to JDK 9, looking up - * annotations directly on a {@link Parameter} will fail for inner class - * constructors. - * - *

Bug in {@code javac} on JDK versions prior to JDK 9

- * - *

The parameter annotations array in the compiled byte code for the user's - * test class excludes an entry for the implicit enclosing instance - * parameter for an inner class constructor. - * - *

Workaround

- * - *

JUnit provides a workaround for this off-by-one error by helping extension - * authors to access annotations on the preceding {@link Parameter} object (i.e., - * {@code index - 1}). The {@linkplain #getIndex() current index} must never be - * zero in such situations since JUnit Jupiter should never ask a - * {@code ParameterResolver} to resolve a parameter for the implicit enclosing - * instance parameter. - * - *

WARNING

- * - *

The {@code AnnotatedElement} returned by this method should never be cast and - * treated as a {@code Parameter} since the metadata (e.g., {@link Parameter#getName()}, - * {@link Parameter#getType()}, etc.) will not match those for the declared parameter - * at the given index in an inner class constructor. - * - * @return the actual {@code Parameter} for this context, or the effective - * {@code Parameter} if the aforementioned bug is detected - */ - private AnnotatedElement getEffectiveAnnotatedParameter() { - Executable executable = getDeclaringExecutable(); - - if (executable instanceof Constructor && isInnerClass(executable.getDeclaringClass()) - && executable.getParameterAnnotations().length == executable.getParameterCount() - 1) { - - Preconditions.condition(this.index != 0, - "A ParameterContext should never be created for parameter index 0 in an inner class constructor"); - - return executable.getParameters()[this.index - 1]; - } - - return this.parameter; + return AnnotationUtils.findRepeatableAnnotations(this.parameter, this.index, annotationType); } @Override diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/ExtensionRegistrar.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/ExtensionRegistrar.java index cf4908dab32..3d6815c3d31 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/ExtensionRegistrar.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/ExtensionRegistrar.java @@ -23,6 +23,18 @@ @API(status = INTERNAL, since = "5.5") public interface ExtensionRegistrar { + /** + * Instantiate an extension of the given type using its default constructor + * and register it in the registry. + * + *

A new {@link Extension} should not be registered if an extension of the + * given type already exists in the registry or a parent registry. + * + * @param extensionType the type of extension to register + * @since 5.8 + */ + void registerExtension(Class extensionType); + /** * Register the supplied {@link Extension}, without checking if an extension * of that type has already been registered. diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/MutableExtensionRegistry.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/MutableExtensionRegistry.java index 58432a0206d..b7eed9ae951 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/MutableExtensionRegistry.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/MutableExtensionRegistry.java @@ -139,16 +139,8 @@ private Stream streamLocal(Class extensionType) { // @formatter:on } - /** - * Instantiate an extension of the given type using its default constructor - * and register it in this registry. - * - *

A new {@link Extension} will not be registered if an extension of the - * given type already exists in this registry or a parent registry. - * - * @param extensionType the type of extension to register - */ - void registerExtension(Class extensionType) { + @Override + public void registerExtension(Class extensionType) { if (!isAlreadyRegistered(extensionType)) { registerLocalExtension(ReflectionUtils.newInstance(extensionType)); } diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java new file mode 100644 index 00000000000..72f6377dd31 --- /dev/null +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java @@ -0,0 +1,551 @@ +/* + * Copyright 2015-2021 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.engine.extension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; + +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; +import org.junit.jupiter.api.extension.TestTemplateInvocationContext; +import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider; +import org.junit.jupiter.engine.AbstractJupiterTestEngineTests; +import org.junit.jupiter.engine.execution.injection.sample.LongParameterResolver; + +/** + * Integration tests that verify support for extension registration via + * {@link ExtendWith @ExtendWith} on annotations on parameters and fields. + * + * @since 5.8 + */ +class ExtensionRegistrationViaParametersAndFieldsTests extends AbstractJupiterTestEngineTests { + + static final List instantiationSequence = new ArrayList<>(); + + @Test + void constructorParameter() { + assertOneTestSucceeded(ConstructorParameterTestCase.class); + } + + @Test + void constructorParameterForNestedTestClass() { + assertOneTestSucceeded(NestedConstructorParameterTestCase.class); + } + + @Test + void beforeAllMethodParameter() { + assertOneTestSucceeded(BeforeAllParameterTestCase.class); + } + + @Test + void afterAllMethodParameter() { + assertOneTestSucceeded(AfterAllParameterTestCase.class); + } + + @Test + void beforeEachMethodParameter() { + assertOneTestSucceeded(BeforeEachParameterTestCase.class); + } + + @Test + void afterEachMethodParameter() { + assertOneTestSucceeded(AfterEachParameterTestCase.class); + } + + @Test + void testMethodParameter() { + assertOneTestSucceeded(TestMethodParameterTestCase.class); + } + + @Test + void testFactoryMethodParameter() { + assertTestsSucceeded(TestFactoryMethodParameterTestCase.class, 2); + } + + @Test + void testTemplateMethodParameter() { + assertTestsSucceeded(TestTemplateMethodParameterTestCase.class, 2); + } + + @Test + void registrationOrder() { + instantiationSequence.clear(); + + assertOneTestSucceeded(AllInOneTestCase.class); + assertThat(instantiationSequence).containsExactly("ConstructorParameter", "BeforeAllParameter", + "AfterAllParameter", "BeforeEachParameter", "AfterEachParameter", "TestParameter"); + } + + private void assertOneTestSucceeded(Class testClass) { + assertTestsSucceeded(testClass, 1); + } + + private void assertTestsSucceeded(Class testClass, int expected) { + executeTestsForClass(testClass).testEvents().assertStatistics( + stats -> stats.started(expected).succeeded(expected).skipped(0).aborted(0).failed(0)); + } + + // ------------------------------------------------------------------- + + /** + * The {@link MagicParameter.Extension} is first registered for the constructor + * and then used for lifecycle and test methods. + */ + @ExtendWith(LongParameterResolver.class) + static class ConstructorParameterTestCase { + + ConstructorParameterTestCase(@MagicParameter("constructor") String text) { + assertThat(text).isEqualTo("ConstructorParameterTestCase-0-constructor"); + } + + @BeforeEach + void beforeEach(String text, TestInfo testInfo) { + assertThat(text).isEqualTo("beforeEach-0-enigma"); + assertThat(testInfo).isNotNull(); + } + + @Test + void test(TestInfo testInfo, String text) { + assertThat(testInfo).isNotNull(); + assertThat(text).isEqualTo("test-1-enigma"); + } + + /** + * Redeclaring {@code @MagicParameter} should not result in a + * {@link ParameterResolutionException}. + */ + @AfterEach + void afterEach(Long number, TestInfo testInfo, @MagicParameter("method") String text) { + assertThat(number).isEqualTo(42L); + assertThat(testInfo).isNotNull(); + assertThat(text).isEqualTo("afterEach-2-method"); + } + + } + + /** + * The {@link MagicParameter.Extension} is first registered for the constructor + * and then used for lifecycle and test methods. + */ + @Nested + @ExtendWith(LongParameterResolver.class) + class NestedConstructorParameterTestCase { + + NestedConstructorParameterTestCase(@MagicParameter("constructor") String text) { + // Index is 1 instead of 0, since constructors for non-static nested classes + // receive a reference to the enclosing instance as the first argument: this$0 + assertThat(text).isEqualTo("NestedConstructorParameterTestCase-1-constructor"); + } + + @BeforeEach + void beforeEach(String text, TestInfo testInfo) { + assertThat(text).isEqualTo("beforeEach-0-enigma"); + assertThat(testInfo).isNotNull(); + } + + @Test + void test(TestInfo testInfo, String text) { + assertThat(testInfo).isNotNull(); + assertThat(text).isEqualTo("test-1-enigma"); + } + + /** + * Redeclaring {@code @MagicParameter} should not result in a + * {@link ParameterResolutionException}. + */ + @AfterEach + void afterEach(Long number, TestInfo testInfo, @MagicParameter("method") String text) { + assertThat(number).isEqualTo(42L); + assertThat(testInfo).isNotNull(); + assertThat(text).isEqualTo("afterEach-2-method"); + } + + } + + /** + * The {@link MagicParameter.Extension} is first registered for the {@code @BeforeAll} + * method and then used for other lifecycle methods and test methods. + */ + @ExtendWith(LongParameterResolver.class) + static class BeforeAllParameterTestCase { + + @BeforeAll + static void beforeAll(@MagicParameter("method") String text, TestInfo testInfo) { + assertThat(text).isEqualTo("beforeAll-0-method"); + assertThat(testInfo).isNotNull(); + } + + @BeforeEach + void beforeEach(String text, TestInfo testInfo) { + assertThat(text).isEqualTo("beforeEach-0-enigma"); + assertThat(testInfo).isNotNull(); + } + + @Test + void test(TestInfo testInfo, String text) { + assertThat(testInfo).isNotNull(); + assertThat(text).isEqualTo("test-1-enigma"); + } + + /** + * Redeclaring {@code @MagicParameter} should not result in a + * {@link ParameterResolutionException}. + */ + @AfterEach + void afterEach(Long number, TestInfo testInfo, @MagicParameter("method") String text) { + assertThat(number).isEqualTo(42L); + assertThat(testInfo).isNotNull(); + assertThat(text).isEqualTo("afterEach-2-method"); + } + + @AfterAll + static void afterAll(String text, TestInfo testInfo) { + assertThat(text).isEqualTo("afterAll-0-enigma"); + assertThat(testInfo).isNotNull(); + } + + } + + /** + * The {@link MagicParameter.Extension} is first registered for the {@code @AfterAll} method. + */ + @ExtendWith(LongParameterResolver.class) + static class AfterAllParameterTestCase { + + @Test + void test(TestInfo testInfo, String text) { + assertThat(testInfo).isNotNull(); + assertThat(text).isEqualTo("test-1-enigma"); + } + + @AfterAll + static void afterAll(Long number, TestInfo testInfo, @MagicParameter("method") String text) { + assertThat(number).isEqualTo(42L); + assertThat(testInfo).isNotNull(); + assertThat(text).isEqualTo("afterAll-2-method"); + } + + } + + /** + * The {@link MagicParameter.Extension} is first registered for the {@code @BeforeEach} + * method and then used for other lifecycle methods and test methods. + */ + @ExtendWith(LongParameterResolver.class) + static class BeforeEachParameterTestCase { + + @BeforeEach + void beforeEach(@MagicParameter("method") String text, TestInfo testInfo) { + assertThat(text).isEqualTo("beforeEach-0-method"); + assertThat(testInfo).isNotNull(); + } + + @Test + void test(TestInfo testInfo, String text) { + assertThat(testInfo).isNotNull(); + assertThat(text).isEqualTo("test-1-enigma"); + } + + /** + * Redeclaring {@code @MagicParameter} should not result in a + * {@link ParameterResolutionException}. + */ + @AfterEach + void afterEach(Long number, TestInfo testInfo, @MagicParameter("method") String text) { + assertThat(number).isEqualTo(42L); + assertThat(testInfo).isNotNull(); + assertThat(text).isEqualTo("afterEach-2-method"); + } + + } + + /** + * The {@link MagicParameter.Extension} is first registered for the {@code @AfterEach} method. + */ + @ExtendWith(LongParameterResolver.class) + static class AfterEachParameterTestCase { + + @Test + void test() { + } + + @AfterEach + void afterEach(Long number, TestInfo testInfo, @MagicParameter("method") String text) { + assertThat(number).isEqualTo(42L); + assertThat(testInfo).isNotNull(); + assertThat(text).isEqualTo("afterEach-2-method"); + } + + } + + /** + * The {@link MagicParameter.Extension} is first registered for the {@code @Test} + * method and then used for after-each lifecycle methods. + */ + @ExtendWith(LongParameterResolver.class) + static class TestMethodParameterTestCase { + + @Test + void test(TestInfo testInfo, @MagicParameter("method") String text) { + assertThat(testInfo).isNotNull(); + assertThat(text).isEqualTo("test-1-method"); + } + + /** + * Redeclaring {@code @MagicParameter} should not result in a + * {@link ParameterResolutionException}. + */ + @AfterEach + void afterEach(Long number, TestInfo testInfo, String text) { + assertThat(number).isEqualTo(42L); + assertThat(testInfo).isNotNull(); + assertThat(text).isEqualTo("afterEach-2-enigma"); + } + + } + + /** + * The {@link MagicParameter.Extension} is first registered for the {@code @TestFactory} + * method and then used for after-each lifecycle methods. + */ + @ExtendWith(LongParameterResolver.class) + static class TestFactoryMethodParameterTestCase { + + @TestFactory + Stream testFactory(TestInfo testInfo, @MagicParameter("method") String text) { + assertThat(testInfo).isNotNull(); + assertThat(text).isEqualTo("testFactory-1-method"); + + return IntStream.of(2, 4).mapToObj(num -> dynamicTest("" + num, () -> assertTrue(num % 2 == 0))); + } + + /** + * Redeclaring {@code @MagicParameter} should not result in a + * {@link ParameterResolutionException}. + */ + @AfterEach + void afterEach(Long number, TestInfo testInfo, String text) { + assertThat(number).isEqualTo(42L); + assertThat(testInfo).isNotNull(); + assertThat(text).isEqualTo("afterEach-2-enigma"); + } + + } + + /** + * The {@link MagicParameter.Extension} is first registered for the {@code @TestTemplate} + * method and then used for after-each lifecycle methods. + */ + @ExtendWith(LongParameterResolver.class) + static class TestTemplateMethodParameterTestCase { + + @TestTemplate + @ExtendWith(TwoInvocationsContextProvider.class) + void testTemplate(TestInfo testInfo, @MagicParameter("method") String text) { + assertThat(testInfo).isNotNull(); + assertThat(text).isEqualTo("testTemplate-1-method"); + } + + /** + * Redeclaring {@code @MagicParameter} should not result in a + * {@link ParameterResolutionException}. + */ + @AfterEach + void afterEach(Long number, TestInfo testInfo, String text) { + assertThat(number).isEqualTo(42L); + assertThat(testInfo).isNotNull(); + assertThat(text).isEqualTo("afterEach-2-enigma"); + } + + } + + private static class TwoInvocationsContextProvider implements TestTemplateInvocationContextProvider { + + @Override + public boolean supportsTestTemplate(ExtensionContext context) { + return true; + } + + @Override + public Stream provideTestTemplateInvocationContexts(ExtensionContext context) { + return Stream.of(emptyTestTemplateInvocationContext(), emptyTestTemplateInvocationContext()); + } + + private static TestTemplateInvocationContext emptyTestTemplateInvocationContext() { + return new TestTemplateInvocationContext() { + }; + } + } + + static class AllInOneTestCase { + + AllInOneTestCase(@ConstructorParameter String text) { + assertThat(text).isNotNull(); + } + + @BeforeAll + static void beforeAll(@BeforeAllParameter String text) { + assertThat(text).isNotNull(); + } + + @BeforeEach + void beforeEach(@BeforeEachParameter String text) { + assertThat(text).isNotNull(); + } + + @Test + void test(@TestParameter String text) { + assertThat(text).isNotNull(); + } + + @AfterEach + void afterEach(@AfterEachParameter String text) { + assertThat(text).isNotNull(); + } + + @AfterAll + static void afterAll(@AfterAllParameter String text) { + assertThat(text).isNotNull(); + } + + } + +} + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(MagicParameter.Extension.class) +@interface MagicParameter { + + String value(); + + class Extension implements ParameterResolver { + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { + return parameterContext.getParameter().getType() == String.class; + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { + String text = parameterContext.findAnnotation(MagicParameter.class)// + .map(MagicParameter::value)// + .orElse("enigma"); + Executable declaringExecutable = parameterContext.getDeclaringExecutable(); + String name = declaringExecutable instanceof Constructor + ? declaringExecutable.getDeclaringClass().getSimpleName() + : declaringExecutable.getName(); + return String.format("%s-%d-%s", name, parameterContext.getIndex(), text); + } + } +} + +@SuppressWarnings("unused") +class BaseExtension implements ParameterResolver { + + private final Class annotationType; + + @SuppressWarnings("unchecked") + BaseExtension() { + Type genericSuperclass = getClass().getGenericSuperclass(); + this.annotationType = (Class) ((ParameterizedType) genericSuperclass).getActualTypeArguments()[0]; + + ExtensionRegistrationViaParametersAndFieldsTests.instantiationSequence// + .add(getClass().getEnclosingClass().getSimpleName()); + } + + @Override + public final boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { + return parameterContext.isAnnotated(this.annotationType) + && parameterContext.getParameter().getType() == String.class; + } + + @Override + public final Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { + return "enigma"; + } +} + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(ConstructorParameter.Extension.class) +@interface ConstructorParameter { + class Extension extends BaseExtension { + } +} + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(BeforeAllParameter.Extension.class) +@interface BeforeAllParameter { + class Extension extends BaseExtension { + } +} + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(AfterAllParameter.Extension.class) +@interface AfterAllParameter { + class Extension extends BaseExtension { + } +} + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(BeforeEachParameter.Extension.class) +@interface BeforeEachParameter { + class Extension extends BaseExtension { + } +} + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(AfterEachParameter.Extension.class) +@interface AfterEachParameter { + class Extension extends BaseExtension { + } +} + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(TestParameter.Extension.class) +@interface TestParameter { + class Extension extends BaseExtension { + } +} diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/AnnotationUtils.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/AnnotationUtils.java index 849b12d85dc..fd6bde1e937 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/AnnotationUtils.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/AnnotationUtils.java @@ -19,8 +19,11 @@ import java.lang.annotation.Inherited; import java.lang.annotation.Repeatable; import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.lang.reflect.Parameter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -78,6 +81,14 @@ public static boolean isAnnotated(Optional element, return findAnnotation(element, annotationType).isPresent(); } + /** + * @since 1.8 + * @see #findAnnotation(Parameter, int, Class) + */ + public static boolean isAnnotated(Parameter parameter, int index, Class annotationType) { + return findAnnotation(parameter, index, annotationType).isPresent(); + } + /** * Determine if an annotation of {@code annotationType} is either * present or meta-present on the supplied @@ -107,6 +118,16 @@ public static Optional findAnnotation(Optional Optional findAnnotation(Parameter parameter, int index, + Class annotationType) { + + return findAnnotation(getEffectiveAnnotatedParameter(parameter, index), annotationType); + } + /** * @see org.junit.platform.commons.support.AnnotationSupport#findAnnotation(AnnotatedElement, Class) */ @@ -237,6 +258,16 @@ public static List findRepeatableAnnotations(Optional< return findRepeatableAnnotations(element.get(), annotationType); } + /** + * @since 1.8 + * @see #findRepeatableAnnotations(AnnotatedElement, Class) + */ + public static List findRepeatableAnnotations(Parameter parameter, int index, + Class annotationType) { + + return findRepeatableAnnotations(getEffectiveAnnotatedParameter(parameter, index), annotationType); + } + /** * @see org.junit.platform.commons.support.AnnotationSupport#findRepeatableAnnotations(AnnotatedElement, Class) */ @@ -357,6 +388,52 @@ private static boolean isRepeatableAnnotationContainer(ClassBug in {@code javac} on JDK versions prior to JDK 9 + * + *

The parameter annotations array in the compiled byte code for the user's + * class excludes an entry for the implicit enclosing instance + * parameter for an inner class constructor. + * + *

Workaround

+ * + *

This method provides a workaround for this off-by-one error by helping + * JUnit maintainers and extension authors to access annotations on the preceding + * {@link Parameter} object (i.e., {@code index - 1}). The {@code index} must + * never be zero in such situations since this method should never attempt to + * resolve an effective parameter for the implicit enclosing instance + * parameter. + * + *

WARNING

+ * + *

The {@code AnnotatedElement} returned by this method should never be cast and + * treated as a {@code Parameter} since the metadata (e.g., {@link Parameter#getName()}, + * {@link Parameter#getType()}, etc.) will not match those for the declared parameter + * at the given index in an inner class constructor. + * + * @return the supplied {@code Parameter}, or the effective {@code Parameter} + * if the aforementioned bug is detected + * @since 1.8 + */ + private static AnnotatedElement getEffectiveAnnotatedParameter(Parameter parameter, int index) { + Preconditions.notNull(parameter, "Parameter must not be null"); + Executable executable = parameter.getDeclaringExecutable(); + + if (executable instanceof Constructor && isInnerClass(executable.getDeclaringClass()) + && executable.getParameterAnnotations().length == executable.getParameterCount() - 1) { + + Preconditions.condition(index != 0, "Parameter index must not be 0 for an inner class constructor"); + + return executable.getParameters()[index - 1]; + } + + return parameter; + } + /** * @see org.junit.platform.commons.support.AnnotationSupport#findPublicAnnotatedFields(Class, Class, Class) */ From 5c4d39468616fe066f6bf05b7a848739f07a9ab0 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sun, 1 Aug 2021 20:19:28 +0200 Subject: [PATCH 02/12] Support extension registration via @ExtendWith on fields Issue: #864 --- .../descriptor/ClassBasedTestDescriptor.java | 8 +- .../engine/descriptor/ExtensionUtils.java | 34 ++- ...gistrationViaParametersAndFieldsTests.java | 242 ++++++++++++++++-- 3 files changed, 244 insertions(+), 40 deletions(-) diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java index cd71b2b9d5e..2eb71183bd2 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java @@ -162,10 +162,14 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte this.afterAllMethods = findAfterAllMethods(this.testClass, this.lifecycle == Lifecycle.PER_METHOD); this.beforeAllMethods.forEach(method -> registerExtensionsFromExecutableParameters(registry, method)); - this.afterAllMethods.forEach(method -> registerExtensionsFromExecutableParameters(registry, method)); - + // Since registerBeforeEachMethodAdapters() and registerAfterEachMethodAdapters() also + // invoke registerExtensionsFromExecutableParameters(), we invoke those methods before + // invoking registerExtensionsFromExecutableParameters() for @BeforeAll methods, + // thereby ensuring proper registration order for extensions registered via @ExtendWith + // on parameters in lifecycle methods. registerBeforeEachMethodAdapters(registry); registerAfterEachMethodAdapters(registry); + this.afterAllMethods.forEach(method -> registerExtensionsFromExecutableParameters(registry, method)); ThrowableCollector throwableCollector = createThrowableCollector(); ClassExtensionContext extensionContext = new ClassExtensionContext(context.getExtensionContext(), diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java index 951a3cb4afb..f662543f9e4 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java @@ -11,9 +11,11 @@ package org.junit.jupiter.engine.descriptor; import static java.util.stream.Collectors.toList; -import static org.junit.platform.commons.util.AnnotationUtils.findAnnotatedFields; import static org.junit.platform.commons.util.AnnotationUtils.findAnnotation; import static org.junit.platform.commons.util.AnnotationUtils.findRepeatableAnnotations; +import static org.junit.platform.commons.util.AnnotationUtils.isAnnotated; +import static org.junit.platform.commons.util.ReflectionUtils.HierarchyTraversalMode.TOP_DOWN; +import static org.junit.platform.commons.util.ReflectionUtils.findFields; import static org.junit.platform.commons.util.ReflectionUtils.getDeclaredConstructor; import static org.junit.platform.commons.util.ReflectionUtils.isNotPrivate; import static org.junit.platform.commons.util.ReflectionUtils.tryToReadFieldValue; @@ -99,24 +101,34 @@ static void registerExtensionsFromFields(ExtensionRegistrar registrar, Class Predicate predicate = (instance == null ? ReflectionUtils::isStatic : ReflectionUtils::isNotStatic); - // Ensure that the list is modifiable, since findAnnotatedFields() returns an unmodifiable list. - List fields = new ArrayList<>(findAnnotatedFields(clazz, RegisterExtension.class, predicate)); + // Ensure that the list is modifiable, since findFields() returns an unmodifiable list. + List fields = new ArrayList<>(findFields(clazz, predicate, TOP_DOWN)); // Sort fields based on @Order. fields.sort(orderComparator); + // @formatter:off fields.forEach(field -> { - Preconditions.condition(isNotPrivate(field), - () -> String.format( + findRepeatableAnnotations(field, ExtendWith.class).stream() + .map(ExtendWith::value) + .flatMap(Arrays::stream) + .forEach(registrar::registerExtension); + }); + + fields.stream() + .filter(field -> isAnnotated(field, RegisterExtension.class)) + .forEach(field -> { + Preconditions.condition(isNotPrivate(field), () -> String.format( "Failed to register extension via @RegisterExtension field [%s]: field must not be private.", field)); - tryToReadFieldValue(field, instance).ifSuccess(value -> { - Preconditions.condition(value instanceof Extension, () -> String.format( - "Failed to register extension via @RegisterExtension field [%s]: field value's type [%s] must implement an [%s] API.", - field, (value != null ? value.getClass().getName() : null), Extension.class.getName())); - registrar.registerExtension((Extension) value, field); + tryToReadFieldValue(field, instance).ifSuccess(value -> { + Preconditions.condition(value instanceof Extension, () -> String.format( + "Failed to register extension via @RegisterExtension field [%s]: field value's type [%s] must implement an [%s] API.", + field, (value != null ? value.getClass().getName() : null), Extension.class.getName())); + registrar.registerExtension((Extension) value, field); + }); }); - }); + // @formatter:on } /** diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java index 72f6377dd31..8de7c199198 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java @@ -10,9 +10,12 @@ package org.junit.jupiter.engine.extension; +import static java.util.stream.Collectors.toList; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.DynamicTest.dynamicTest; +import static org.junit.platform.commons.util.AnnotationUtils.findAnnotatedFields; +import static org.junit.platform.commons.util.ReflectionUtils.makeAccessible; import java.lang.annotation.Annotation; import java.lang.annotation.ElementType; @@ -21,10 +24,13 @@ import java.lang.annotation.Target; import java.lang.reflect.Constructor; import java.lang.reflect.Executable; +import java.lang.reflect.Field; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; -import java.util.ArrayList; import java.util.List; +import java.util.function.Predicate; +import java.util.logging.Level; +import java.util.logging.LogRecord; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -37,7 +43,11 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ParameterContext; @@ -45,8 +55,12 @@ import org.junit.jupiter.api.extension.ParameterResolver; import org.junit.jupiter.api.extension.TestTemplateInvocationContext; import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider; +import org.junit.jupiter.api.fixtures.TrackLogRecords; import org.junit.jupiter.engine.AbstractJupiterTestEngineTests; import org.junit.jupiter.engine.execution.injection.sample.LongParameterResolver; +import org.junit.platform.commons.logging.LogRecordListener; +import org.junit.platform.commons.util.ExceptionUtils; +import org.junit.platform.commons.util.ReflectionUtils; /** * Integration tests that verify support for extension registration via @@ -56,8 +70,6 @@ */ class ExtensionRegistrationViaParametersAndFieldsTests extends AbstractJupiterTestEngineTests { - static final List instantiationSequence = new ArrayList<>(); - @Test void constructorParameter() { assertOneTestSucceeded(ConstructorParameterTestCase.class); @@ -104,12 +116,43 @@ void testTemplateMethodParameter() { } @Test - void registrationOrder() { - instantiationSequence.clear(); + void staticField() { + assertOneTestSucceeded(StaticFieldTestCase.class); + } + + @Test + void instanceField() { + assertOneTestSucceeded(InstanceFieldTestCase.class); + } - assertOneTestSucceeded(AllInOneTestCase.class); - assertThat(instantiationSequence).containsExactly("ConstructorParameter", "BeforeAllParameter", - "AfterAllParameter", "BeforeEachParameter", "AfterEachParameter", "TestParameter"); + @Test + void fieldsWithTestInstancePerClass() { + assertOneTestSucceeded(TestInstancePerClassFieldTestCase.class); + } + + @Test + @TrackLogRecords + void registrationOrder(LogRecordListener listener) { + assertOneTestSucceeded(AllInOneWithTestInstancePerMethodTestCase.class); + assertThat(getRegisteredLocalExtensions(listener))// + .containsExactly("StaticField", "ConstructorParameter", "BeforeAllParameter", "BeforeEachParameter", + "AfterEachParameter", "AfterAllParameter", "TestParameter", "InstanceField"); + + listener.clear(); + assertOneTestSucceeded(AllInOneWithTestInstancePerClassTestCase.class); + assertThat(getRegisteredLocalExtensions(listener))// + .containsExactly("StaticField", "ConstructorParameter", "BeforeAllParameter", "BeforeEachParameter", + "AfterEachParameter", "AfterAllParameter", "InstanceField", "TestParameter"); + } + + private List getRegisteredLocalExtensions(LogRecordListener listener) { + // @formatter:off + return listener.stream(MutableExtensionRegistry.class, Level.FINER) + .map(LogRecord::getMessage) + .filter(message -> message.contains("local extension")) + .map(message -> message.substring(message.lastIndexOf('.') + 1, message.indexOf("$"))) + .collect(toList()); + // @formatter:on } private void assertOneTestSucceeded(Class testClass) { @@ -413,39 +456,130 @@ private static TestTemplateInvocationContext emptyTestTemplateInvocationContext( } } - static class AllInOneTestCase { + /** + * The {@link MagicField.Extension} is registered via a static field. + */ + static class StaticFieldTestCase { - AllInOneTestCase(@ConstructorParameter String text) { - assertThat(text).isNotNull(); + @MagicField + static String staticField1; + + @MagicField + static String staticField2; + + @BeforeAll + static void beforeAll() { + assertThat(staticField1).isEqualTo("beforeAll - staticField1"); + assertThat(staticField2).isEqualTo("beforeAll - staticField2"); + } + + @Test + void test() { + assertThat(staticField1).isEqualTo("beforeAll - staticField1"); + assertThat(staticField2).isEqualTo("beforeAll - staticField2"); + } + } + + /** + * The {@link MagicField.Extension} is registered via an instance field. + */ + static class InstanceFieldTestCase { + + @MagicField + String instanceField1; + + @MagicField + String instanceField2; + + @Test + void test() { + assertThat(instanceField1).isEqualTo("beforeEach - instanceField1"); + assertThat(instanceField2).isEqualTo("beforeEach - instanceField2"); + } + } + + /** + * The {@link MagicField.Extension} is registered via a static field and + * an instance field. + */ + @TestInstance(Lifecycle.PER_CLASS) + static class TestInstancePerClassFieldTestCase { + + @MagicField + static String staticField; + + @MagicField + String instanceField; + + @BeforeAll + void beforeAll() { + assertThat(staticField).isEqualTo("beforeAll - staticField"); + assertThat(instanceField).isNull(); + } + + @Test + void test() { + assertThat(staticField).isEqualTo("beforeAll - staticField"); + assertThat(instanceField).isEqualTo("beforeEach - instanceField"); + } + } + + @TestInstance(Lifecycle.PER_METHOD) + static class AllInOneWithTestInstancePerMethodTestCase { + + @StaticField + static String staticField; + + @InstanceField + String instanceField; + + AllInOneWithTestInstancePerMethodTestCase(@ConstructorParameter String text) { + assertThat(text).isEqualTo("enigma"); } @BeforeAll static void beforeAll(@BeforeAllParameter String text) { - assertThat(text).isNotNull(); + assertThat(text).isEqualTo("enigma"); + assertThat(staticField).isEqualTo("beforeAll - staticField"); } @BeforeEach void beforeEach(@BeforeEachParameter String text) { - assertThat(text).isNotNull(); + assertThat(text).isEqualTo("enigma"); + assertThat(staticField).isEqualTo("beforeAll - staticField"); + assertThat(instanceField).isEqualTo("beforeEach - instanceField"); } @Test void test(@TestParameter String text) { - assertThat(text).isNotNull(); + assertThat(text).isEqualTo("enigma"); + assertThat(staticField).isEqualTo("beforeAll - staticField"); + assertThat(instanceField).isEqualTo("beforeEach - instanceField"); } @AfterEach void afterEach(@AfterEachParameter String text) { - assertThat(text).isNotNull(); + assertThat(text).isEqualTo("enigma"); + assertThat(staticField).isEqualTo("beforeAll - staticField"); + assertThat(instanceField).isEqualTo("beforeEach - instanceField"); } @AfterAll static void afterAll(@AfterAllParameter String text) { - assertThat(text).isNotNull(); + assertThat(text).isEqualTo("enigma"); + assertThat(staticField).isEqualTo("beforeAll - staticField"); } } + @TestInstance(Lifecycle.PER_CLASS) + static class AllInOneWithTestInstancePerClassTestCase extends AllInOneWithTestInstancePerMethodTestCase { + + AllInOneWithTestInstancePerClassTestCase(@ConstructorParameter String text) { + super(text); + } + } + } @Target(ElementType.PARAMETER) @@ -477,17 +611,14 @@ public Object resolveParameter(ParameterContext parameterContext, ExtensionConte } @SuppressWarnings("unused") -class BaseExtension implements ParameterResolver { +class BaseParameterExtension implements ParameterResolver { private final Class annotationType; @SuppressWarnings("unchecked") - BaseExtension() { + BaseParameterExtension() { Type genericSuperclass = getClass().getGenericSuperclass(); this.annotationType = (Class) ((ParameterizedType) genericSuperclass).getActualTypeArguments()[0]; - - ExtensionRegistrationViaParametersAndFieldsTests.instantiationSequence// - .add(getClass().getEnclosingClass().getSimpleName()); } @Override @@ -506,7 +637,7 @@ public final Object resolveParameter(ParameterContext parameterContext, Extensio @Retention(RetentionPolicy.RUNTIME) @ExtendWith(ConstructorParameter.Extension.class) @interface ConstructorParameter { - class Extension extends BaseExtension { + class Extension extends BaseParameterExtension { } } @@ -514,7 +645,7 @@ class Extension extends BaseExtension { @Retention(RetentionPolicy.RUNTIME) @ExtendWith(BeforeAllParameter.Extension.class) @interface BeforeAllParameter { - class Extension extends BaseExtension { + class Extension extends BaseParameterExtension { } } @@ -522,7 +653,7 @@ class Extension extends BaseExtension { @Retention(RetentionPolicy.RUNTIME) @ExtendWith(AfterAllParameter.Extension.class) @interface AfterAllParameter { - class Extension extends BaseExtension { + class Extension extends BaseParameterExtension { } } @@ -530,7 +661,7 @@ class Extension extends BaseExtension { @Retention(RetentionPolicy.RUNTIME) @ExtendWith(BeforeEachParameter.Extension.class) @interface BeforeEachParameter { - class Extension extends BaseExtension { + class Extension extends BaseParameterExtension { } } @@ -538,7 +669,7 @@ class Extension extends BaseExtension { @Retention(RetentionPolicy.RUNTIME) @ExtendWith(AfterEachParameter.Extension.class) @interface AfterEachParameter { - class Extension extends BaseExtension { + class Extension extends BaseParameterExtension { } } @@ -546,6 +677,63 @@ class Extension extends BaseExtension { @Retention(RetentionPolicy.RUNTIME) @ExtendWith(TestParameter.Extension.class) @interface TestParameter { - class Extension extends BaseExtension { + class Extension extends BaseParameterExtension { + } +} + +class BaseFieldExtension implements BeforeAllCallback, BeforeEachCallback { + + private final Class annotationType; + + @SuppressWarnings("unchecked") + BaseFieldExtension() { + Type genericSuperclass = getClass().getGenericSuperclass(); + this.annotationType = (Class) ((ParameterizedType) genericSuperclass).getActualTypeArguments()[0]; + } + + @Override + public final void beforeAll(ExtensionContext context) throws Exception { + injectFields("beforeAll", context.getRequiredTestClass(), null, ReflectionUtils::isStatic); + } + + @Override + public final void beforeEach(ExtensionContext context) throws Exception { + injectFields("beforeEach", context.getRequiredTestClass(), context.getRequiredTestInstance(), + ReflectionUtils::isNotStatic); + } + + private void injectFields(String trigger, Class testClass, Object instance, Predicate predicate) { + findAnnotatedFields(testClass, this.annotationType, predicate).forEach(field -> { + try { + makeAccessible(field).set(instance, trigger + " - " + field.getName()); + } + catch (Throwable t) { + ExceptionUtils.throwAsUncheckedException(t); + } + }); + } +} + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(MagicField.Extension.class) +@interface MagicField { + class Extension extends BaseFieldExtension { + } +} + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(InstanceField.Extension.class) +@interface InstanceField { + class Extension extends BaseFieldExtension { + } +} + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(StaticField.Extension.class) +@interface StaticField { + class Extension extends BaseFieldExtension { } } From a197a3dd4d19bcb83bffa90d424f955b69a62a31 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Mon, 2 Aug 2021 18:11:54 +0200 Subject: [PATCH 03/12] Test extension registration for double @Nested constructor parameter Issue: #864 --- ...gistrationViaParametersAndFieldsTests.java | 43 +++++++++++++++++-- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java index 8de7c199198..442425ce2b1 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java @@ -77,7 +77,7 @@ void constructorParameter() { @Test void constructorParameterForNestedTestClass() { - assertOneTestSucceeded(NestedConstructorParameterTestCase.class); + assertTestsSucceeded(NestedConstructorParameterTestCase.class, 2); } @Test @@ -210,10 +210,11 @@ void afterEach(Long number, TestInfo testInfo, @MagicParameter("method") String @ExtendWith(LongParameterResolver.class) class NestedConstructorParameterTestCase { - NestedConstructorParameterTestCase(@MagicParameter("constructor") String text) { - // Index is 1 instead of 0, since constructors for non-static nested classes + NestedConstructorParameterTestCase(TestInfo testInfo, @MagicParameter("constructor") String text) { + assertThat(testInfo).isNotNull(); + // Index is 2 instead of 1, since constructors for non-static nested classes // receive a reference to the enclosing instance as the first argument: this$0 - assertThat(text).isEqualTo("NestedConstructorParameterTestCase-1-constructor"); + assertThat(text).isEqualTo("NestedConstructorParameterTestCase-2-constructor"); } @BeforeEach @@ -239,6 +240,40 @@ void afterEach(Long number, TestInfo testInfo, @MagicParameter("method") String assertThat(text).isEqualTo("afterEach-2-method"); } + @Nested + class DoublyNestedConstructorParameterTestCase { + + DoublyNestedConstructorParameterTestCase(TestInfo testInfo, String text) { + assertThat(testInfo).isNotNull(); + // Index is 2 instead of 1, since constructors for non-static nested classes + // receive a reference to the enclosing instance as the first argument: this$0 + assertThat(text).isEqualTo("DoublyNestedConstructorParameterTestCase-2-enigma"); + } + + @BeforeEach + void beforeEach(String text, TestInfo testInfo) { + assertThat(text).isEqualTo("beforeEach-0-enigma"); + assertThat(testInfo).isNotNull(); + } + + @Test + void test(TestInfo testInfo, String text) { + assertThat(testInfo).isNotNull(); + assertThat(text).isEqualTo("test-1-enigma"); + } + + /** + * Redeclaring {@code @MagicParameter} should not result in a + * {@link ParameterResolutionException}. + */ + @AfterEach + void afterEach(Long number, TestInfo testInfo, @MagicParameter("method") String text) { + assertThat(number).isEqualTo(42L); + assertThat(testInfo).isNotNull(); + assertThat(text).isEqualTo("afterEach-2-method"); + } + } + } /** From 08e15c18fe70c9d5b67d15f3c6157a759df473c5 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Mon, 2 Aug 2021 18:35:37 +0200 Subject: [PATCH 04/12] Update Javadoc for ExtensionUtils.registerExtensionsFromFields() Issue: #864 --- .../org/junit/jupiter/engine/descriptor/ExtensionUtils.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java index f662543f9e4..39a17bce464 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java @@ -85,7 +85,8 @@ static MutableExtensionRegistry populateNewExtensionRegistryFromExtendWithAnnota /** * Register extensions using the supplied registrar from fields in the supplied - * class that are annotated with {@link RegisterExtension @RegisterExtension}. + * class that are meta-annotated with {@link ExtendWith @ExtendWith} or + * annotated with {@link RegisterExtension @RegisterExtension}. * *

The extensions will be sorted according to {@link Order @Order} semantics * prior to registration. From 89dd293ccc6f38f7c8cdf723f633ee5254beb21d Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Mon, 2 Aug 2021 19:29:42 +0200 Subject: [PATCH 05/12] Test implicit vs. explicit extension registration order for fields This commit introduces extensions registered explicitly via @RegisterExtension to verify the respective ordering with extensions registered implicitly via @ExtendWith on fields. Issue: #864 --- ...gistrationViaParametersAndFieldsTests.java | 71 +++++++++++++++++-- 1 file changed, 66 insertions(+), 5 deletions(-) diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java index 442425ce2b1..0346fa8890e 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java @@ -40,6 +40,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; import org.junit.jupiter.api.TestInfo; @@ -49,10 +50,12 @@ import org.junit.jupiter.api.extension.BeforeAllCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.Extension; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolutionException; import org.junit.jupiter.api.extension.ParameterResolver; +import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.extension.TestTemplateInvocationContext; import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider; import org.junit.jupiter.api.fixtures.TrackLogRecords; @@ -135,14 +138,38 @@ void fieldsWithTestInstancePerClass() { void registrationOrder(LogRecordListener listener) { assertOneTestSucceeded(AllInOneWithTestInstancePerMethodTestCase.class); assertThat(getRegisteredLocalExtensions(listener))// - .containsExactly("StaticField", "ConstructorParameter", "BeforeAllParameter", "BeforeEachParameter", - "AfterEachParameter", "AfterAllParameter", "TestParameter", "InstanceField"); + .containsExactly(// + "StaticField", // @ExtendWith on static field + "ClassLevelExtension1", // @RegisterExtension on static field + "ClassLevelExtension2", // @RegisterExtension on static field + "ConstructorParameter", // @ExtendWith on parameter in constructor + "BeforeAllParameter", // @ExtendWith on parameter in static @BeforeAll method + "BeforeEachParameter", // @ExtendWith on parameter in @BeforeEach method + "AfterEachParameter", // @ExtendWith on parameter in @AfterEach method + "AfterAllParameter", // @ExtendWith on parameter in static @AfterAll method + "TestParameter", // @ExtendWith on parameter in @Test method + "InstanceField", // @ExtendWith on instance field + "InstanceLevelExtension1", // @RegisterExtension on instance field + "InstanceLevelExtension2"// @RegisterExtension on instance field + ); listener.clear(); assertOneTestSucceeded(AllInOneWithTestInstancePerClassTestCase.class); assertThat(getRegisteredLocalExtensions(listener))// - .containsExactly("StaticField", "ConstructorParameter", "BeforeAllParameter", "BeforeEachParameter", - "AfterEachParameter", "AfterAllParameter", "InstanceField", "TestParameter"); + .containsExactly(// + "StaticField", // @ExtendWith on static field + "ClassLevelExtension1", // @RegisterExtension on static field + "ClassLevelExtension2", // @RegisterExtension on static field + "ConstructorParameter", // @ExtendWith on parameter in constructor + "BeforeAllParameter", // @ExtendWith on parameter in static @BeforeAll method + "BeforeEachParameter", // @ExtendWith on parameter in @BeforeEach method + "AfterEachParameter", // @ExtendWith on parameter in @AfterEach method + "AfterAllParameter", // @ExtendWith on parameter in static @AfterAll method + "InstanceField", // @ExtendWith on instance field + "InstanceLevelExtension1", // @RegisterExtension on instance field + "InstanceLevelExtension2", // @RegisterExtension on instance field + "TestParameter" // @ExtendWith on parameter in @Test method + ); } private List getRegisteredLocalExtensions(LogRecordListener listener) { @@ -150,7 +177,13 @@ private List getRegisteredLocalExtensions(LogRecordListener listener) { return listener.stream(MutableExtensionRegistry.class, Level.FINER) .map(LogRecord::getMessage) .filter(message -> message.contains("local extension")) - .map(message -> message.substring(message.lastIndexOf('.') + 1, message.indexOf("$"))) + .map(message -> { + message = message.replaceAll("from source .+", ""); + int indexOfDollarSign = message.indexOf("$"); + int indexOfAtSign = message.indexOf("@"); + int endIndex = (indexOfDollarSign > 1 ? indexOfDollarSign : indexOfAtSign); + return message.substring(message.lastIndexOf('.') + 1, endIndex); + }) .collect(toList()); // @formatter:on } @@ -568,6 +601,22 @@ static class AllInOneWithTestInstancePerMethodTestCase { @InstanceField String instanceField; + @RegisterExtension + @Order(1) + static Extension classLevelExtension1 = new ClassLevelExtension1(); + + @RegisterExtension + @Order(2) + static Extension classLevelExtension2 = new ClassLevelExtension2(); + + @RegisterExtension + @Order(1) + Extension instanceLevelExtension1 = new InstanceLevelExtension1(); + + @RegisterExtension + @Order(2) + Extension instanceLevelExtension2 = new InstanceLevelExtension2(); + AllInOneWithTestInstancePerMethodTestCase(@ConstructorParameter String text) { assertThat(text).isEqualTo("enigma"); } @@ -772,3 +821,15 @@ class Extension extends BaseFieldExtension { class Extension extends BaseFieldExtension { } } + +class ClassLevelExtension1 implements Extension { +} + +class ClassLevelExtension2 implements Extension { +} + +class InstanceLevelExtension1 implements Extension { +} + +class InstanceLevelExtension2 implements Extension { +} From 5fa36d1762fddf6be8c6eebc30e4bfa25ed8827a Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Tue, 3 Aug 2021 17:00:38 +0200 Subject: [PATCH 06/12] Polishing Issue: #864 --- .../descriptor/ClassBasedTestDescriptor.java | 2 +- .../engine/descriptor/ExtensionUtils.java | 3 +- ...gistrationViaParametersAndFieldsTests.java | 33 ++++++++++--------- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java index 2eb71183bd2..91039207877 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java @@ -164,7 +164,7 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte this.beforeAllMethods.forEach(method -> registerExtensionsFromExecutableParameters(registry, method)); // Since registerBeforeEachMethodAdapters() and registerAfterEachMethodAdapters() also // invoke registerExtensionsFromExecutableParameters(), we invoke those methods before - // invoking registerExtensionsFromExecutableParameters() for @BeforeAll methods, + // invoking registerExtensionsFromExecutableParameters() for @AfterAll methods, // thereby ensuring proper registration order for extensions registered via @ExtendWith // on parameters in lifecycle methods. registerBeforeEachMethodAdapters(registry); diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java index 39a17bce464..3d866805b7c 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java @@ -148,7 +148,8 @@ static void registerExtensionsFromConstructorParameters(ExtensionRegistrar regis /** * Register extensions using the supplied registrar from parameters in the * supplied {@link Executable} (i.e., a {@link java.lang.reflect.Constructor} - * or {@link java.lang.reflect.Method}) that are annotated with{@link ExtendWith @ExtendWith}. + * or {@link java.lang.reflect.Method}) that are meta-annotated with + * {@link ExtendWith @ExtendWith}. * * @param registrar the registrar with which to register the extensions; never {@code null} * @param executable the constructor or method whose parameters should be searched; never {@code null} diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java index 0346fa8890e..4c450970602 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java @@ -354,7 +354,10 @@ static void afterAll(String text, TestInfo testInfo) { } /** - * The {@link MagicParameter.Extension} is first registered for the {@code @AfterAll} method. + * The {@link MagicParameter.Extension} is first registered for the {@code @AfterAll} + * method, but that registration occurs before the test method is invoked, which + * allows the string parameters in the after-each and test methods to be resolved + * by the {@link MagicParameter.Extension} as well. */ @ExtendWith(LongParameterResolver.class) static class AfterAllParameterTestCase { @@ -365,6 +368,13 @@ void test(TestInfo testInfo, String text) { assertThat(text).isEqualTo("test-1-enigma"); } + @AfterEach + void afterEach(Long number, TestInfo testInfo, String text) { + assertThat(number).isEqualTo(42L); + assertThat(testInfo).isNotNull(); + assertThat(text).isEqualTo("afterEach-2-enigma"); + } + @AfterAll static void afterAll(Long number, TestInfo testInfo, @MagicParameter("method") String text) { assertThat(number).isEqualTo(42L); @@ -407,13 +417,18 @@ void afterEach(Long number, TestInfo testInfo, @MagicParameter("method") String } /** - * The {@link MagicParameter.Extension} is first registered for the {@code @AfterEach} method. + * The {@link MagicParameter.Extension} is first registered for the {@code @AfterEach} + * method, but that registration occurs before the test method is invoked, which + * allows the test method's parameter to be resolved by the {@link MagicParameter.Extension} + * as well. */ @ExtendWith(LongParameterResolver.class) static class AfterEachParameterTestCase { @Test - void test() { + void test(TestInfo testInfo, String text) { + assertThat(testInfo).isNotNull(); + assertThat(text).isEqualTo("test-1-enigma"); } @AfterEach @@ -438,10 +453,6 @@ void test(TestInfo testInfo, @MagicParameter("method") String text) { assertThat(text).isEqualTo("test-1-method"); } - /** - * Redeclaring {@code @MagicParameter} should not result in a - * {@link ParameterResolutionException}. - */ @AfterEach void afterEach(Long number, TestInfo testInfo, String text) { assertThat(number).isEqualTo(42L); @@ -466,10 +477,6 @@ Stream testFactory(TestInfo testInfo, @MagicParameter("method") Str return IntStream.of(2, 4).mapToObj(num -> dynamicTest("" + num, () -> assertTrue(num % 2 == 0))); } - /** - * Redeclaring {@code @MagicParameter} should not result in a - * {@link ParameterResolutionException}. - */ @AfterEach void afterEach(Long number, TestInfo testInfo, String text) { assertThat(number).isEqualTo(42L); @@ -493,10 +500,6 @@ void testTemplate(TestInfo testInfo, @MagicParameter("method") String text) { assertThat(text).isEqualTo("testTemplate-1-method"); } - /** - * Redeclaring {@code @MagicParameter} should not result in a - * {@link ParameterResolutionException}. - */ @AfterEach void afterEach(Long number, TestInfo testInfo, String text) { assertThat(number).isEqualTo(42L); From 4ae957dcb84163c6e36d4b6438346da5daa7e75b Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Wed, 4 Aug 2021 15:46:45 +0200 Subject: [PATCH 07/12] Remove code duplication in ExtensionUtils Issue: #864 --- .../engine/descriptor/ExtensionUtils.java | 39 ++++++++++--------- .../extension/MutableExtensionRegistry.java | 2 +- .../extension/ExtensionRegistryTests.java | 12 +++--- 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java index 3d866805b7c..dab0d31d329 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java @@ -10,7 +10,6 @@ package org.junit.jupiter.engine.descriptor; -import static java.util.stream.Collectors.toList; import static org.junit.platform.commons.util.AnnotationUtils.findAnnotation; import static org.junit.platform.commons.util.AnnotationUtils.findRepeatableAnnotations; import static org.junit.platform.commons.util.AnnotationUtils.isAnnotated; @@ -25,11 +24,11 @@ import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Predicate; +import java.util.stream.Stream; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.extension.ExtendWith; @@ -73,14 +72,7 @@ static MutableExtensionRegistry populateNewExtensionRegistryFromExtendWithAnnota Preconditions.notNull(parentRegistry, "Parent ExtensionRegistry must not be null"); Preconditions.notNull(annotatedElement, "AnnotatedElement must not be null"); - // @formatter:off - List> extensionTypes = findRepeatableAnnotations(annotatedElement, ExtendWith.class).stream() - .map(ExtendWith::value) - .flatMap(Arrays::stream) - .collect(toList()); - // @formatter:on - - return MutableExtensionRegistry.createRegistryFrom(parentRegistry, extensionTypes); + return MutableExtensionRegistry.createRegistryFrom(parentRegistry, streamExtensionTypes(annotatedElement)); } /** @@ -109,12 +101,9 @@ static void registerExtensionsFromFields(ExtensionRegistrar registrar, Class fields.sort(orderComparator); // @formatter:off - fields.forEach(field -> { - findRepeatableAnnotations(field, ExtendWith.class).stream() - .map(ExtendWith::value) - .flatMap(Arrays::stream) - .forEach(registrar::registerExtension); - }); + fields.stream() + .flatMap(ExtensionUtils::streamExtensionTypes) + .forEach(registrar::registerExtension); fields.stream() .filter(field -> isAnnotated(field, RegisterExtension.class)) @@ -164,13 +153,25 @@ static void registerExtensionsFromExecutableParameters(ExtensionRegistrar regist // @formatter:off Arrays.stream(executable.getParameters()) .map(parameter -> findRepeatableAnnotations(parameter, index.getAndIncrement(), ExtendWith.class)) - .flatMap(Collection::stream) - .map(ExtendWith::value) - .flatMap(Arrays::stream) + .flatMap(ExtensionUtils::streamExtensionTypes) .forEach(registrar::registerExtension); // @formatter:on } + /** + * @since 5.8 + */ + private static Stream> streamExtensionTypes(AnnotatedElement annotatedElement) { + return streamExtensionTypes(findRepeatableAnnotations(annotatedElement, ExtendWith.class)); + } + + /** + * @since 5.8 + */ + private static Stream> streamExtensionTypes(List extendWithAnnotations) { + return extendWithAnnotations.stream().map(ExtendWith::value).flatMap(Arrays::stream); + } + /** * @since 5.4 */ diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/MutableExtensionRegistry.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/MutableExtensionRegistry.java index b7eed9ae951..6a739ca3958 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/MutableExtensionRegistry.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/MutableExtensionRegistry.java @@ -95,7 +95,7 @@ private static void registerAutoDetectedExtensions(MutableExtensionRegistry exte * @return a new {@code ExtensionRegistry}; never {@code null} */ public static MutableExtensionRegistry createRegistryFrom(MutableExtensionRegistry parentRegistry, - List> extensionTypes) { + Stream> extensionTypes) { Preconditions.notNull(parentRegistry, "parentRegistry must not be null"); diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistryTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistryTests.java index 54856358ffb..8a9883df634 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistryTests.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistryTests.java @@ -10,9 +10,6 @@ package org.junit.jupiter.engine.extension; -import static java.util.Arrays.asList; -import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -23,6 +20,7 @@ import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.BeforeAllCallback; @@ -104,12 +102,12 @@ void extensionsAreInheritedFromParent() { MutableExtensionRegistry parent = registry; parent.registerExtension(MyExtension.class); - MutableExtensionRegistry child = createRegistryFrom(parent, singletonList(YourExtension.class)); + MutableExtensionRegistry child = createRegistryFrom(parent, Stream.of(YourExtension.class)); assertExtensionRegistered(child, MyExtension.class); assertExtensionRegistered(child, YourExtension.class); assertEquals(2, countExtensions(child, MyExtensionApi.class)); - ExtensionRegistry grandChild = createRegistryFrom(child, emptyList()); + ExtensionRegistry grandChild = createRegistryFrom(child, Stream.empty()); assertExtensionRegistered(grandChild, MyExtension.class); assertExtensionRegistered(grandChild, YourExtension.class); assertEquals(2, countExtensions(grandChild, MyExtensionApi.class)); @@ -121,12 +119,12 @@ void registeringSameExtensionImplementationInParentAndChildDoesNotResultInDuplic parent.registerExtension(MyExtension.class); assertEquals(1, countExtensions(parent, MyExtensionApi.class)); - MutableExtensionRegistry child = createRegistryFrom(parent, asList(MyExtension.class, YourExtension.class)); + MutableExtensionRegistry child = createRegistryFrom(parent, Stream.of(MyExtension.class, YourExtension.class)); assertExtensionRegistered(child, MyExtension.class); assertExtensionRegistered(child, YourExtension.class); assertEquals(2, countExtensions(child, MyExtensionApi.class)); - ExtensionRegistry grandChild = createRegistryFrom(child, asList(MyExtension.class, YourExtension.class)); + ExtensionRegistry grandChild = createRegistryFrom(child, Stream.of(MyExtension.class, YourExtension.class)); assertExtensionRegistered(grandChild, MyExtension.class); assertExtensionRegistered(grandChild, YourExtension.class); assertEquals(2, countExtensions(grandChild, MyExtensionApi.class)); From 3480fc80bb2bda415f875b1ac0de8b671d332b1d Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Fri, 6 Aug 2021 14:41:10 +0200 Subject: [PATCH 08/12] Allow @RegisterExtension fields to be private Prior to this commit, an exception was thrown if a @RegisterExtension field was declared private. This commit removes this restriction. Closes #2688 See #864, #2680 --- .../docs/asciidoc/user-guide/extensions.adoc | 10 +++++----- .../api/extension/RegisterExtension.java | 4 ++-- .../engine/descriptor/ExtensionUtils.java | 4 ---- ...RegistrationViaParametersAndFieldsTests.java | 8 ++++---- .../ProgrammaticExtensionRegistrationTests.java | 17 ++++++----------- 5 files changed, 17 insertions(+), 26 deletions(-) diff --git a/documentation/src/docs/asciidoc/user-guide/extensions.adoc b/documentation/src/docs/asciidoc/user-guide/extensions.adoc index b31110332c1..e0b956fe93f 100644 --- a/documentation/src/docs/asciidoc/user-guide/extensions.adoc +++ b/documentation/src/docs/asciidoc/user-guide/extensions.adoc @@ -125,8 +125,8 @@ registered last and _after_ callback extensions to be registered first, relative programmatically registered extensions. ==== -NOTE: `@RegisterExtension` fields must not be `private` or `null` (at evaluation time) but -may be either `static` or non-static. +NOTE: `@RegisterExtension` fields must not be `null` (at evaluation time) but may be +either `static` or non-static. [[extensions-registration-programmatic-static-fields]] ===== Static Fields @@ -159,9 +159,9 @@ include::{testDir}/example/registration/WebServerDemo.java[tags=user_guide] The Kotlin programming language does not have the concept of a `static` field. However, the compiler can be instructed to generate static fields using annotations. Since, as -stated earlier, `@RegisterExtension` fields must not be `private` nor `null`, one -**cannot** use the `@JvmStatic` annotation in Kotlin as it generates `private` fields. -Rather, the `@JvmField` annotation must be used. +stated earlier, `@RegisterExtension` fields must not be `null`, one **cannot** use the +`@JvmStatic` annotation in Kotlin as it generates `private` fields. Rather, the +`@JvmField` annotation must be used. The following example is a version of the `WebServerDemo` from the previous section that has been ported to Kotlin. diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/RegisterExtension.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/RegisterExtension.java index dfd3ef157c8..55977b63b7c 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/RegisterExtension.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/RegisterExtension.java @@ -30,8 +30,8 @@ * to pass arguments to the extension's constructor, {@code static} factory * method, or builder API. * - *

{@code @RegisterExtension} fields must not be {@code private} or - * {@code null} (when evaluated) but may be either {@code static} or non-static. + *

{@code @RegisterExtension} fields must not be {@code null} (when evaluated) + * but may be either {@code static} or non-static. * *

Static Fields

* diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java index dab0d31d329..748ffecae47 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java @@ -16,7 +16,6 @@ import static org.junit.platform.commons.util.ReflectionUtils.HierarchyTraversalMode.TOP_DOWN; import static org.junit.platform.commons.util.ReflectionUtils.findFields; import static org.junit.platform.commons.util.ReflectionUtils.getDeclaredConstructor; -import static org.junit.platform.commons.util.ReflectionUtils.isNotPrivate; import static org.junit.platform.commons.util.ReflectionUtils.tryToReadFieldValue; import java.lang.reflect.AnnotatedElement; @@ -108,9 +107,6 @@ static void registerExtensionsFromFields(ExtensionRegistrar registrar, Class fields.stream() .filter(field -> isAnnotated(field, RegisterExtension.class)) .forEach(field -> { - Preconditions.condition(isNotPrivate(field), () -> String.format( - "Failed to register extension via @RegisterExtension field [%s]: field must not be private.", - field)); tryToReadFieldValue(field, instance).ifSuccess(value -> { Preconditions.condition(value instanceof Extension, () -> String.format( "Failed to register extension via @RegisterExtension field [%s]: field value's type [%s] must implement an [%s] API.", diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java index 4c450970602..f3980b87059 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java @@ -533,7 +533,7 @@ private static TestTemplateInvocationContext emptyTestTemplateInvocationContext( static class StaticFieldTestCase { @MagicField - static String staticField1; + private static String staticField1; @MagicField static String staticField2; @@ -560,7 +560,7 @@ static class InstanceFieldTestCase { String instanceField1; @MagicField - String instanceField2; + private String instanceField2; @Test void test() { @@ -606,7 +606,7 @@ static class AllInOneWithTestInstancePerMethodTestCase { @RegisterExtension @Order(1) - static Extension classLevelExtension1 = new ClassLevelExtension1(); + private static Extension classLevelExtension1 = new ClassLevelExtension1(); @RegisterExtension @Order(2) @@ -614,7 +614,7 @@ static class AllInOneWithTestInstancePerMethodTestCase { @RegisterExtension @Order(1) - Extension instanceLevelExtension1 = new InstanceLevelExtension1(); + private Extension instanceLevelExtension1 = new InstanceLevelExtension1(); @RegisterExtension @Order(2) diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ProgrammaticExtensionRegistrationTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ProgrammaticExtensionRegistrationTests.java index 29fcadc9385..6c958899256 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ProgrammaticExtensionRegistrationTests.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ProgrammaticExtensionRegistrationTests.java @@ -167,8 +167,7 @@ void classLevelWithFieldThatDoesNotImplementAnExtensionApi() { @Test void instanceLevelWithPrivateField() { Class testClass = InstanceLevelExtensionRegistrationWithPrivateFieldTestCase.class; - executeTestsForClass(testClass).testEvents().assertThatEvents().haveExactly(1, finishedWithFailure( - instanceOf(PreconditionViolationException.class), message(expectedNotPrivateMessage(testClass)))); + executeTestsForClass(testClass).testEvents().assertStatistics(stats -> stats.succeeded(1)); } /** @@ -177,8 +176,7 @@ void instanceLevelWithPrivateField() { @Test void classLevelWithPrivateField() { Class testClass = ClassLevelExtensionRegistrationWithPrivateFieldTestCase.class; - executeTestsForClass(testClass).containerEvents().assertThatEvents().haveExactly(1, finishedWithFailure( - instanceOf(PreconditionViolationException.class), message(expectedNotPrivateMessage(testClass)))); + executeTestsForClass(testClass).testEvents().assertStatistics(stats -> stats.succeeded(1)); } @Test @@ -219,11 +217,6 @@ void classLevelWithNonExtensionFieldValue() { instanceOf(PreconditionViolationException.class), message(expectedMessage(testClass, String.class)))); } - private String expectedNotPrivateMessage(Class testClass) { - return "Failed to register extension via @RegisterExtension field [" + field(testClass) - + "]: field must not be private."; - } - private String expectedMessage(Class testClass, Class valueType) { return "Failed to register extension via @RegisterExtension field [" + field(testClass) + "]: field value's type [" + (valueType != null ? valueType.getName() : null) + "] must implement an [" @@ -602,14 +595,16 @@ void test() { static class InstanceLevelExtensionRegistrationWithPrivateFieldTestCase extends AbstractTestCase { @RegisterExtension - private Extension extension; + private Extension extension = new Extension() { + }; } static class ClassLevelExtensionRegistrationWithPrivateFieldTestCase extends AbstractTestCase { @RegisterExtension - private static Extension extension; + private static Extension extension = new Extension() { + }; } From cc9690bd9581c10443d70367d8493c0f1bb9f1f4 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Fri, 6 Aug 2021 15:12:57 +0200 Subject: [PATCH 09/12] Allow @ExtendWith to be declared directly on fields and parameters Prior to this commit, @ExtendWith could only be declared on types (i.e., classes, interfaces, annotations) and methods. @ExtendWith can now be declared directly on fields and parameters as well. See #864, #2680 --- .../docs/asciidoc/user-guide/extensions.adoc | 6 +- .../jupiter/api/extension/ExtendWith.java | 10 ++- ...gistrationViaParametersAndFieldsTests.java | 87 ++++++++++++++----- 3 files changed, 74 insertions(+), 29 deletions(-) diff --git a/documentation/src/docs/asciidoc/user-guide/extensions.adoc b/documentation/src/docs/asciidoc/user-guide/extensions.adoc index e0b956fe93f..a0addd083ed 100644 --- a/documentation/src/docs/asciidoc/user-guide/extensions.adoc +++ b/documentation/src/docs/asciidoc/user-guide/extensions.adoc @@ -21,8 +21,10 @@ Java's <> mechanism. Developers can register one or more extensions _declaratively_ by annotating a test interface, test class, test method, or custom _<>_ with `@ExtendWith(...)` and supplying class references for the extensions -to register. +annotation>>_ with `@ExtendWith(...)` and supplying class references for the extensions to +register. As of JUnit Jupiter 5.8, `@ExtendWith` may also be declared on fields and on +parameters in test class constructors, in test methods, and in `@BeforeAll`, `@AfterAll`, +`@BeforeEach`, and `@AfterEach` lifecycle methods. For example, to register a custom `RandomParametersExtension` for a particular test method, you would annotate the test method as follows. diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtendWith.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtendWith.java index 3139e3832d7..ed1d5764ef2 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtendWith.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtendWith.java @@ -24,8 +24,12 @@ /** * {@code @ExtendWith} is a {@linkplain Repeatable repeatable} annotation - * that is used to register {@linkplain Extension extensions} for the - * annotated test class or test method. + * that is used to register {@linkplain Extension extensions} for the annotated + * test class, test interface, test method, field, or parameter. + * + *

Annotated parameters are supported in test class constructors, in test + * methods, and in {@code @BeforeAll}, {@code @AfterAll}, {@code @BeforeEach}, + * and {@code @AfterEach} lifecycle methods. * *

Supported Extension APIs

*
    @@ -49,7 +53,7 @@ * @see RegisterExtension * @see Extension */ -@Target({ ElementType.TYPE, ElementType.METHOD }) +@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER }) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java index f3980b87059..e090ad09939 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java @@ -139,7 +139,8 @@ void registrationOrder(LogRecordListener listener) { assertOneTestSucceeded(AllInOneWithTestInstancePerMethodTestCase.class); assertThat(getRegisteredLocalExtensions(listener))// .containsExactly(// - "StaticField", // @ExtendWith on static field + "StaticField1", // @ExtendWith on static field + "StaticField2", // @ExtendWith on static field "ClassLevelExtension1", // @RegisterExtension on static field "ClassLevelExtension2", // @RegisterExtension on static field "ConstructorParameter", // @ExtendWith on parameter in constructor @@ -148,7 +149,8 @@ void registrationOrder(LogRecordListener listener) { "AfterEachParameter", // @ExtendWith on parameter in @AfterEach method "AfterAllParameter", // @ExtendWith on parameter in static @AfterAll method "TestParameter", // @ExtendWith on parameter in @Test method - "InstanceField", // @ExtendWith on instance field + "InstanceField1", // @ExtendWith on instance field + "InstanceField2", // @ExtendWith on instance field "InstanceLevelExtension1", // @RegisterExtension on instance field "InstanceLevelExtension2"// @RegisterExtension on instance field ); @@ -157,7 +159,8 @@ void registrationOrder(LogRecordListener listener) { assertOneTestSucceeded(AllInOneWithTestInstancePerClassTestCase.class); assertThat(getRegisteredLocalExtensions(listener))// .containsExactly(// - "StaticField", // @ExtendWith on static field + "StaticField1", // @ExtendWith on static field + "StaticField2", // @ExtendWith on static field "ClassLevelExtension1", // @RegisterExtension on static field "ClassLevelExtension2", // @RegisterExtension on static field "ConstructorParameter", // @ExtendWith on parameter in constructor @@ -165,7 +168,8 @@ void registrationOrder(LogRecordListener listener) { "BeforeEachParameter", // @ExtendWith on parameter in @BeforeEach method "AfterEachParameter", // @ExtendWith on parameter in @AfterEach method "AfterAllParameter", // @ExtendWith on parameter in static @AfterAll method - "InstanceField", // @ExtendWith on instance field + "InstanceField1", // @ExtendWith on instance field + "InstanceField2", // @ExtendWith on instance field "InstanceLevelExtension1", // @RegisterExtension on instance field "InstanceLevelExtension2", // @RegisterExtension on instance field "TestParameter" // @ExtendWith on parameter in @Test method @@ -598,11 +602,19 @@ void test() { @TestInstance(Lifecycle.PER_METHOD) static class AllInOneWithTestInstancePerMethodTestCase { - @StaticField - static String staticField; + @StaticField1 + static String staticField1; - @InstanceField - String instanceField; + @StaticField2 + @ExtendWith(StaticField2.Extension.class) + static String staticField2; + + @InstanceField1 + String instanceField1; + + @InstanceField2 + @ExtendWith(InstanceField2.Extension.class) + String instanceField2; @RegisterExtension @Order(1) @@ -625,36 +637,44 @@ static class AllInOneWithTestInstancePerMethodTestCase { } @BeforeAll - static void beforeAll(@BeforeAllParameter String text) { + static void beforeAll(@ExtendWith(BeforeAllParameter.Extension.class) @BeforeAllParameter String text) { assertThat(text).isEqualTo("enigma"); - assertThat(staticField).isEqualTo("beforeAll - staticField"); + assertThat(staticField1).isEqualTo("beforeAll - staticField1"); + assertThat(staticField2).isEqualTo("beforeAll - staticField2"); } @BeforeEach void beforeEach(@BeforeEachParameter String text) { assertThat(text).isEqualTo("enigma"); - assertThat(staticField).isEqualTo("beforeAll - staticField"); - assertThat(instanceField).isEqualTo("beforeEach - instanceField"); + assertThat(staticField1).isEqualTo("beforeAll - staticField1"); + assertThat(staticField2).isEqualTo("beforeAll - staticField2"); + assertThat(instanceField1).isEqualTo("beforeEach - instanceField1"); + assertThat(instanceField2).isEqualTo("beforeEach - instanceField2"); } @Test void test(@TestParameter String text) { assertThat(text).isEqualTo("enigma"); - assertThat(staticField).isEqualTo("beforeAll - staticField"); - assertThat(instanceField).isEqualTo("beforeEach - instanceField"); + assertThat(staticField1).isEqualTo("beforeAll - staticField1"); + assertThat(staticField2).isEqualTo("beforeAll - staticField2"); + assertThat(instanceField1).isEqualTo("beforeEach - instanceField1"); + assertThat(instanceField2).isEqualTo("beforeEach - instanceField2"); } @AfterEach void afterEach(@AfterEachParameter String text) { assertThat(text).isEqualTo("enigma"); - assertThat(staticField).isEqualTo("beforeAll - staticField"); - assertThat(instanceField).isEqualTo("beforeEach - instanceField"); + assertThat(staticField1).isEqualTo("beforeAll - staticField1"); + assertThat(staticField2).isEqualTo("beforeAll - staticField2"); + assertThat(instanceField1).isEqualTo("beforeEach - instanceField1"); + assertThat(instanceField2).isEqualTo("beforeEach - instanceField2"); } @AfterAll static void afterAll(@AfterAllParameter String text) { assertThat(text).isEqualTo("enigma"); - assertThat(staticField).isEqualTo("beforeAll - staticField"); + assertThat(staticField1).isEqualTo("beforeAll - staticField1"); + assertThat(staticField2).isEqualTo("beforeAll - staticField2"); } } @@ -730,7 +750,8 @@ class Extension extends BaseParameterExtension { @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) -@ExtendWith(BeforeAllParameter.Extension.class) +// Intentionally NOT annotated as follows +// @ExtendWith(BeforeAllParameter.Extension.class) @interface BeforeAllParameter { class Extension extends BaseParameterExtension { } @@ -811,17 +832,35 @@ class Extension extends BaseFieldExtension { @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) -@ExtendWith(InstanceField.Extension.class) -@interface InstanceField { - class Extension extends BaseFieldExtension { +@ExtendWith(InstanceField1.Extension.class) +@interface InstanceField1 { + class Extension extends BaseFieldExtension { + } +} + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +// Intentionally NOT annotated as follows +// @ExtendWith(InstanceField2.Extension.class) +@interface InstanceField2 { + class Extension extends BaseFieldExtension { + } +} + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(StaticField1.Extension.class) +@interface StaticField1 { + class Extension extends BaseFieldExtension { } } @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) -@ExtendWith(StaticField.Extension.class) -@interface StaticField { - class Extension extends BaseFieldExtension { +// Intentionally NOT annotated as follows +// @ExtendWith(StaticField2.Extension.class) +@interface StaticField2 { + class Extension extends BaseFieldExtension { } } From f149ad675da45f0e951b2b1951d6c2fcda73cf1d Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Fri, 6 Aug 2021 15:45:37 +0200 Subject: [PATCH 10/12] Allow @RegisterExtension/@ExtendWith fields to be sorted relative to each other Prior to this commit, @ExtendWith fields and @RegisterExtension fields were sorted using @Order, but extensions registered via @ExtendWith fields were always registered first (before extensions registered via @RegisterExtension fields). This commit ensures that @RegisterExtension fields and @ExtendWith fields are sorted via @Order relative to each other. See #864, #2680 --- .../engine/descriptor/ExtensionUtils.java | 39 ++++++++----------- ...gistrationViaParametersAndFieldsTests.java | 38 +++++++++--------- 2 files changed, 37 insertions(+), 40 deletions(-) diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java index 748ffecae47..9dd2c1542b5 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java @@ -21,7 +21,6 @@ import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Executable; import java.lang.reflect.Field; -import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.List; @@ -93,28 +92,24 @@ static void registerExtensionsFromFields(ExtensionRegistrar registrar, Class Predicate predicate = (instance == null ? ReflectionUtils::isStatic : ReflectionUtils::isNotStatic); - // Ensure that the list is modifiable, since findFields() returns an unmodifiable list. - List fields = new ArrayList<>(findFields(clazz, predicate, TOP_DOWN)); - - // Sort fields based on @Order. - fields.sort(orderComparator); - - // @formatter:off - fields.stream() - .flatMap(ExtensionUtils::streamExtensionTypes) - .forEach(registrar::registerExtension); - - fields.stream() - .filter(field -> isAnnotated(field, RegisterExtension.class)) - .forEach(field -> { - tryToReadFieldValue(field, instance).ifSuccess(value -> { - Preconditions.condition(value instanceof Extension, () -> String.format( - "Failed to register extension via @RegisterExtension field [%s]: field value's type [%s] must implement an [%s] API.", - field, (value != null ? value.getClass().getName() : null), Extension.class.getName())); - registrar.registerExtension((Extension) value, field); + findFields(clazz, predicate, TOP_DOWN).stream()// + .sorted(orderComparator)// + .forEach(field -> { + List extendWithAnnotations = findRepeatableAnnotations(field, ExtendWith.class); + boolean isExtendWithPresent = !extendWithAnnotations.isEmpty(); + boolean isRegisterExtensionPresent = isAnnotated(field, RegisterExtension.class); + if (isExtendWithPresent) { + streamExtensionTypes(extendWithAnnotations).forEach(registrar::registerExtension); + } + if (isRegisterExtensionPresent) { + tryToReadFieldValue(field, instance).ifSuccess(value -> { + Preconditions.condition(value instanceof Extension, () -> String.format( + "Failed to register extension via @RegisterExtension field [%s]: field value's type [%s] must implement an [%s] API.", + field, (value != null ? value.getClass().getName() : null), Extension.class.getName())); + registrar.registerExtension((Extension) value, field); + }); + } }); - }); - // @formatter:on } /** diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java index e090ad09939..4534aaba9b3 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java @@ -139,39 +139,39 @@ void registrationOrder(LogRecordListener listener) { assertOneTestSucceeded(AllInOneWithTestInstancePerMethodTestCase.class); assertThat(getRegisteredLocalExtensions(listener))// .containsExactly(// - "StaticField1", // @ExtendWith on static field + "ClassLevelExtension2", // @RegisterExtension on static field "StaticField2", // @ExtendWith on static field "ClassLevelExtension1", // @RegisterExtension on static field - "ClassLevelExtension2", // @RegisterExtension on static field + "StaticField1", // @ExtendWith on static field "ConstructorParameter", // @ExtendWith on parameter in constructor "BeforeAllParameter", // @ExtendWith on parameter in static @BeforeAll method "BeforeEachParameter", // @ExtendWith on parameter in @BeforeEach method "AfterEachParameter", // @ExtendWith on parameter in @AfterEach method "AfterAllParameter", // @ExtendWith on parameter in static @AfterAll method "TestParameter", // @ExtendWith on parameter in @Test method - "InstanceField1", // @ExtendWith on instance field - "InstanceField2", // @ExtendWith on instance field "InstanceLevelExtension1", // @RegisterExtension on instance field - "InstanceLevelExtension2"// @RegisterExtension on instance field + "InstanceField1", // @ExtendWith on instance field + "InstanceLevelExtension2", // @RegisterExtension on instance field + "InstanceField2" // @ExtendWith on instance field ); listener.clear(); assertOneTestSucceeded(AllInOneWithTestInstancePerClassTestCase.class); assertThat(getRegisteredLocalExtensions(listener))// .containsExactly(// - "StaticField1", // @ExtendWith on static field + "ClassLevelExtension2", // @RegisterExtension on static field "StaticField2", // @ExtendWith on static field "ClassLevelExtension1", // @RegisterExtension on static field - "ClassLevelExtension2", // @RegisterExtension on static field + "StaticField1", // @ExtendWith on static field "ConstructorParameter", // @ExtendWith on parameter in constructor "BeforeAllParameter", // @ExtendWith on parameter in static @BeforeAll method "BeforeEachParameter", // @ExtendWith on parameter in @BeforeEach method "AfterEachParameter", // @ExtendWith on parameter in @AfterEach method "AfterAllParameter", // @ExtendWith on parameter in static @AfterAll method - "InstanceField1", // @ExtendWith on instance field - "InstanceField2", // @ExtendWith on instance field "InstanceLevelExtension1", // @RegisterExtension on instance field + "InstanceField1", // @ExtendWith on instance field "InstanceLevelExtension2", // @RegisterExtension on instance field + "InstanceField2", // @ExtendWith on instance field "TestParameter" // @ExtendWith on parameter in @Test method ); } @@ -603,33 +603,35 @@ void test() { static class AllInOneWithTestInstancePerMethodTestCase { @StaticField1 + @Order(Integer.MAX_VALUE) static String staticField1; @StaticField2 @ExtendWith(StaticField2.Extension.class) + @Order(3) static String staticField2; + @RegisterExtension + private static Extension classLevelExtension1 = new ClassLevelExtension1(); + + @RegisterExtension + @Order(1) + static Extension classLevelExtension2 = new ClassLevelExtension2(); + @InstanceField1 + @Order(2) String instanceField1; @InstanceField2 @ExtendWith(InstanceField2.Extension.class) String instanceField2; - @RegisterExtension - @Order(1) - private static Extension classLevelExtension1 = new ClassLevelExtension1(); - - @RegisterExtension - @Order(2) - static Extension classLevelExtension2 = new ClassLevelExtension2(); - @RegisterExtension @Order(1) private Extension instanceLevelExtension1 = new InstanceLevelExtension1(); @RegisterExtension - @Order(2) + @Order(3) Extension instanceLevelExtension2 = new InstanceLevelExtension2(); AllInOneWithTestInstancePerMethodTestCase(@ConstructorParameter String text) { From e4804cba6ef66b9742e0197d060ca50e879d9b80 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Mon, 9 Aug 2021 17:01:05 +0200 Subject: [PATCH 11/12] Disallow duplicate registration via @RegisterExtension and @ExtendWith Prior to this commit, @ExtendWith and @RegisterExtension could be declared on a field to register duplicate extensions of the same time, but that scenario likely does not make sense. This commit ensures that @RegisterExtension and @ExtendWith cannot be used to register an extension of the same type for a given field, by throwing a PreconditionViolationException if the user attempts to do so. See #2680 --- .../engine/descriptor/ExtensionUtils.java | 19 +++++-- ...gistrationViaParametersAndFieldsTests.java | 51 ++++++++++++++++++- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java index 9dd2c1542b5..9e73e91b6eb 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java @@ -10,6 +10,7 @@ package org.junit.jupiter.engine.descriptor; +import static java.util.stream.Collectors.toList; import static org.junit.platform.commons.util.AnnotationUtils.findAnnotation; import static org.junit.platform.commons.util.AnnotationUtils.findRepeatableAnnotations; import static org.junit.platform.commons.util.AnnotationUtils.isAnnotated; @@ -95,17 +96,29 @@ static void registerExtensionsFromFields(ExtensionRegistrar registrar, Class findFields(clazz, predicate, TOP_DOWN).stream()// .sorted(orderComparator)// .forEach(field -> { - List extendWithAnnotations = findRepeatableAnnotations(field, ExtendWith.class); - boolean isExtendWithPresent = !extendWithAnnotations.isEmpty(); + List> extensionTypes = streamExtensionTypes(field).collect(toList()); + boolean isExtendWithPresent = !extensionTypes.isEmpty(); boolean isRegisterExtensionPresent = isAnnotated(field, RegisterExtension.class); if (isExtendWithPresent) { - streamExtensionTypes(extendWithAnnotations).forEach(registrar::registerExtension); + extensionTypes.forEach(registrar::registerExtension); } if (isRegisterExtensionPresent) { tryToReadFieldValue(field, instance).ifSuccess(value -> { Preconditions.condition(value instanceof Extension, () -> String.format( "Failed to register extension via @RegisterExtension field [%s]: field value's type [%s] must implement an [%s] API.", field, (value != null ? value.getClass().getName() : null), Extension.class.getName())); + + if (isExtendWithPresent) { + Class valueType = value.getClass(); + extensionTypes.forEach(extensionType -> { + Preconditions.condition(!extensionType.equals(valueType), + () -> String.format("Failed to register extension via field [%s]. " + + "The field registers an extension of type [%s] via @RegisterExtension and @ExtendWith, " + + "but only one registration of a given extension type is permitted.", + field, valueType.getName())); + }); + } + registrar.registerExtension((Extension) value, field); }); } diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java index 4534aaba9b3..670f679f917 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java @@ -16,6 +16,9 @@ import static org.junit.jupiter.api.DynamicTest.dynamicTest; import static org.junit.platform.commons.util.AnnotationUtils.findAnnotatedFields; import static org.junit.platform.commons.util.ReflectionUtils.makeAccessible; +import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure; +import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf; +import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message; import java.lang.annotation.Annotation; import java.lang.annotation.ElementType; @@ -61,13 +64,14 @@ import org.junit.jupiter.api.fixtures.TrackLogRecords; import org.junit.jupiter.engine.AbstractJupiterTestEngineTests; import org.junit.jupiter.engine.execution.injection.sample.LongParameterResolver; +import org.junit.platform.commons.PreconditionViolationException; import org.junit.platform.commons.logging.LogRecordListener; import org.junit.platform.commons.util.ExceptionUtils; import org.junit.platform.commons.util.ReflectionUtils; /** * Integration tests that verify support for extension registration via - * {@link ExtendWith @ExtendWith} on annotations on parameters and fields. + * {@link ExtendWith @ExtendWith} on parameters and fields. * * @since 5.8 */ @@ -133,6 +137,26 @@ void fieldsWithTestInstancePerClass() { assertOneTestSucceeded(TestInstancePerClassFieldTestCase.class); } + @Test + @TrackLogRecords + void multipleRegistrationsViaField(LogRecordListener listener) { + assertOneTestSucceeded(MultipleRegistrationsViaFieldTestCase.class); + assertThat(getRegisteredLocalExtensions(listener)).containsExactly("LongParameterResolver", "DummyExtension"); + } + + @Test + void duplicateRegistrationViaField() { + Class testClass = DuplicateRegistrationViaFieldTestCase.class; + String expectedMessage = "Failed to register extension via field " + + "[org.junit.jupiter.api.extension.Extension " + + "org.junit.jupiter.engine.extension.ExtensionRegistrationViaParametersAndFieldsTests$DuplicateRegistrationViaFieldTestCase.dummy]. " + + "The field registers an extension of type [org.junit.jupiter.engine.extension.DummyExtension] " + + "via @RegisterExtension and @ExtendWith, but only one registration of a given extension type is permitted."; + + executeTestsForClass(testClass).testEvents().assertThatEvents().haveExactly(1, + finishedWithFailure(instanceOf(PreconditionViolationException.class), message(expectedMessage))); + } + @Test @TrackLogRecords void registrationOrder(LogRecordListener listener) { @@ -531,6 +555,28 @@ private static TestTemplateInvocationContext emptyTestTemplateInvocationContext( } } + static class MultipleRegistrationsViaFieldTestCase { + + @ExtendWith(LongParameterResolver.class) + @RegisterExtension + Extension dummy = new DummyExtension(); + + @Test + void test() { + } + } + + static class DuplicateRegistrationViaFieldTestCase { + + @ExtendWith(DummyExtension.class) + @RegisterExtension + Extension dummy = new DummyExtension(); + + @Test + void test() { + } + } + /** * The {@link MagicField.Extension} is registered via a static field. */ @@ -791,6 +837,9 @@ class Extension extends BaseParameterExtension { } } +class DummyExtension implements Extension { +} + class BaseFieldExtension implements BeforeAllCallback, BeforeEachCallback { private final Class annotationType; From b8e44fac9d525303792bd20f864916fc836c4318 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Mon, 9 Aug 2021 18:03:26 +0200 Subject: [PATCH 12/12] Document support for @ExtendWith on parameters and fields Issue: #864 --- .../release-notes/release-notes-5.8.0-M2.adoc | 21 ++- .../docs/asciidoc/user-guide/extensions.adoc | 135 +++++++++++++----- .../registration/KotlinWebServerDemo.kt | 2 +- .../jupiter/api/extension/ExtendWith.java | 35 ++++- .../api/extension/RegisterExtension.java | 4 +- .../engine/descriptor/ExtensionUtils.java | 6 +- 6 files changed, 153 insertions(+), 50 deletions(-) diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.8.0-M2.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.8.0-M2.adoc index 972ec640c00..4c9cbdd43ce 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.8.0-M2.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.8.0-M2.adoc @@ -58,6 +58,9 @@ on GitHub. * Generating Java Flight Recorder events via module `org.junit.platform.jfr` is now also supported on Java 8 Update 262 or higher, in addition to Java 11 or later. See <<../user-guide/index.adoc#running-tests, Flight Recorder Support>> for details. +* Suites executed by the `junit-platform-suite` module will now inherit the configuration + parameters from the parent discovery request. This behavior can be disabled via the + `@DisableParentConfigurationParameters` annotation. [[release-notes-5.8.0-M2-junit-jupiter]] @@ -84,7 +87,11 @@ on GitHub. ==== New Features and Improvements -* `assertDoesNotThrow` now supports suspending functions when called from Kotlin. +* New `assertThrowsExactly()` method in `Assertions` which is a more strict version of + `assertThrows()` that allows you to assert that the exception thrown is of the exact + type specified. +* `assertDoesNotThrow()` in `Assertions` now supports suspending functions when called + from Kotlin. * `@TempDir` can now be used to create multiple temporary directories. Instead of creating a single temporary directory per context (i.e. test class or method) every declaration of the `@TempDir` annotation on a field or method parameter now results in a separate @@ -93,13 +100,15 @@ on GitHub. you can set the `junit.jupiter.tempdir.scope` configuration parameter to `per_context`. * `@TempDir` cleanup resets readable and executable permissions of the root temporary directory and any contained directories instead of failing to delete them. -* `@TempDir` fields may now be private. +* `@TempDir` fields may now be `private`. +* `@ExtendWith` may now be used to register extensions declaratively via fields or + parameters in test class constructors, test methods, and lifecycle methods. See + <<../user-guide/index.adoc#extensions-registration-declarative, Declarative Extension + Registration>> for details. * New `named()` static factory method in the `Named` interface that serves as an _alias_ for `Named.of()`. `named()` is intended to be used via `import static`. * New `class` URI scheme for dynamic test sources. This allows tests to be located using the information available in a `StackTraceElement`. -* New `assertThrowsExactly` method which is a more strict version of `assertThrows` - that allows you to assert that the thrown exception has the exact specified class. * New `autoCloseArguments` attribute in `@ParameterizedTest` to close `AutoCloseable` arguments at the end of the test. This attribute defaults to true. @@ -117,6 +126,4 @@ on GitHub. ==== New Features and Improvements -* Suites executed by the `junit-platform-suite` module will now inherit the - configuration parameters from the parent discovery request. This behaviour can - be disabled via the `@DisableParentConfigurationParameters` annotation. +* ❓ diff --git a/documentation/src/docs/asciidoc/user-guide/extensions.adoc b/documentation/src/docs/asciidoc/user-guide/extensions.adoc index a0addd083ed..c2f1773be25 100644 --- a/documentation/src/docs/asciidoc/user-guide/extensions.adoc +++ b/documentation/src/docs/asciidoc/user-guide/extensions.adoc @@ -22,28 +22,31 @@ Java's <> mechanism. Developers can register one or more extensions _declaratively_ by annotating a test interface, test class, test method, or custom _<>_ with `@ExtendWith(...)` and supplying class references for the extensions to -register. As of JUnit Jupiter 5.8, `@ExtendWith` may also be declared on fields and on +register. As of JUnit Jupiter 5.8, `@ExtendWith` may also be declared on fields or on parameters in test class constructors, in test methods, and in `@BeforeAll`, `@AfterAll`, `@BeforeEach`, and `@AfterEach` lifecycle methods. -For example, to register a custom `RandomParametersExtension` for a particular test -method, you would annotate the test method as follows. +For example, to register a `WebServerExtension` for a particular test method, you would +annotate the test method as follows. We assume the `WebServerExtension` starts a local web +server and injects the server's URL into parameters annotated with `@WebServerUrl`. [source,java,indent=0] ---- -@ExtendWith(RandomParametersExtension.class) @Test -void test(@Random int i) { - // ... +@ExtendWith(WebServerExtension.class) +void getProductList(@WebServerUrl String serverUrl) { + WebClient webClient = new WebClient(); + // Use WebClient to connect to web server using serverUrl and verify response + assertEquals(200, webClient.get(serverUrl + "/products").getResponseStatus()); } ---- -To register a custom `RandomParametersExtension` for all tests in a particular class and -its subclasses, you would annotate the test class as follows. +To register the `WebServerExtension` for all tests in a particular class and its +subclasses, you would annotate the test class as follows. [source,java,indent=0] ---- -@ExtendWith(RandomParametersExtension.class) +@ExtendWith(WebServerExtension.class) class MyTests { // ... } @@ -73,15 +76,16 @@ class MySecondTests { [TIP] .Extension Registration Order ==== -Extensions registered declaratively via `@ExtendWith` will be executed in the order in -which they are declared in the source code. For example, the execution of tests in both -`MyFirstTests` and `MySecondTests` will be extended by the `DatabaseExtension` and -`WebServerExtension`, **in exactly that order**. +Extensions registered declaratively via `@ExtendWith` at the class level, method level, or +parameter level will be executed in the order in which they are declared in the source +code. For example, the execution of tests in both `MyFirstTests` and `MySecondTests` will +be extended by the `DatabaseExtension` and `WebServerExtension`, **in exactly that order**. ==== If you wish to combine multiple extensions in a reusable way, you can define a custom _<>_ and use `@ExtendWith` as a -_meta-annotation_: +_meta-annotation_ as in the following code listing. Then `@DatabaseAndWebServerExtension` +can be used in place of `@ExtendWith({ DatabaseExtension.class, WebServerExtension.class })`. [source,java,indent=0] ---- @@ -92,6 +96,64 @@ public @interface DatabaseAndWebServerExtension { } ---- +The above examples demonstrate how `@ExtendWith` can be applied at the class level or at +the method level; however, for certain use cases it makes sense for an extension to be +registered declaratively at the field or parameter level. Consider a +`RandomNumberExtension` that generates random numbers that can be injected into a field or +via a parameter in a constructor, test method, or lifecycle method. If the extension +provides a `@Random` annotation that is meta-annotated with +`@ExtendWith(RandomNumberExtension.class)` (see listing below), the extension can be used +transparently as in the following `RandomNumberTests` example. + +[source,java,indent=0] +---- +@Target({ ElementType.FIELD, ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(RandomNumberExtension.class) +public @interface Random { +} +---- + +[source,java,indent=0] +---- +class RandomNumberTests { + + // use random number field in test methods and @BeforeEach + // or @AfterEach lifecycle methods + @Random + private int randomNumber1; + + RandomNumberTests(@Random int randomNumber2) { + // use random number in constructor + } + + @BeforeEach + void beforeEach(@Random int randomNumber3) { + // use random number in @BeforeEach method + } + + @Test + void test(@Random int randomNumber4) { + // use random number in test method + } +} +---- + +[TIP] +.Extension Registration Order for `@ExtendWith` on Fields +==== +Extensions registered declaratively via `@ExtendWith` on fields will be ordered relative +to `@RegisterExtension` fields and other `@ExtendWith` fields using an algorithm that is +deterministic but intentionally nonobvious. However, `@ExtendWith` fields can be ordered +using the `@Order` annotation. See the <> tip for `@RegisterExtension` fields for details. +==== + +NOTE: `@ExtendWith` fields may be either `static` or non-static. The documentation on +<> and +<> for +`@RegisterExtension` fields also applies to `@ExtendWith` fields. + [[extensions-registration-programmatic]] ==== Programmatic Extension Registration @@ -108,23 +170,23 @@ extension's constructor, a static factory method, or a builder API. [TIP] .Extension Registration Order ==== -By default, extensions registered programmatically via `@RegisterExtension` will be -ordered using an algorithm that is deterministic but intentionally nonobvious. This -ensures that subsequent runs of a test suite execute extensions in the same order, thereby -allowing for repeatable builds. However, there are times when extensions need to be -registered in an explicit order. To achieve that, annotate `@RegisterExtension` fields -with `{Order}`. - -Any `@RegisterExtension` field not annotated with `@Order` will be ordered using the -_default_ order which has a value of `Integer.MAX_VALUE / 2`. This allows `@Order` -annotated extension fields to be explicitly ordered before or after non-annotated -extension fields. Extensions with an explicit order value less than the default order -value will be registered before non-annotated extensions. Similarly, extensions with an -explicit order value greater than the default order value will be registered after -non-annotated extensions. For example, assigning an extension an explicit order value that -is greater than the default order value allows _before_ callback extensions to be -registered last and _after_ callback extensions to be registered first, relative to other -programmatically registered extensions. +By default, extensions registered programmatically via `@RegisterExtension` or +declaratively via `@ExtendWith` on fields will be ordered using an algorithm that is +deterministic but intentionally nonobvious. This ensures that subsequent runs of a test +suite execute extensions in the same order, thereby allowing for repeatable builds. +However, there are times when extensions need to be registered in an explicit order. To +achieve that, annotate `@RegisterExtension` fields or `@ExtendWith` fields with `{Order}`. + +Any `@RegisterExtension` field or `@ExtendWith` field not annotated with `@Order` will be +ordered using the _default_ order which has a value of `Integer.MAX_VALUE / 2`. This +allows `@Order` annotated extension fields to be explicitly ordered before or after +non-annotated extension fields. Extensions with an explicit order value less than the +default order value will be registered before non-annotated extensions. Similarly, +extensions with an explicit order value greater than the default order value will be +registered after non-annotated extensions. For example, assigning an extension an explicit +order value that is greater than the default order value allows _before_ callback +extensions to be registered last and _after_ callback extensions to be registered first, +relative to other programmatically registered extensions. ==== NOTE: `@RegisterExtension` fields must not be `null` (at evaluation time) but may be @@ -151,19 +213,18 @@ lifecycle methods annotated with `@BeforeAll` or `@AfterAll` as well as `@Before `server` field if necessary. [source,java,indent=0] -.An extension registered via a static field +.Registering an extension via a static field in Java ---- include::{testDir}/example/registration/WebServerDemo.java[tags=user_guide] ---- [[extensions-registration-programmatic-static-fields-kotlin]] -===== Static Fields in Kotlin +====== Static Fields in Kotlin The Kotlin programming language does not have the concept of a `static` field. However, -the compiler can be instructed to generate static fields using annotations. Since, as -stated earlier, `@RegisterExtension` fields must not be `null`, one **cannot** use the -`@JvmStatic` annotation in Kotlin as it generates `private` fields. Rather, the -`@JvmField` annotation must be used. +the compiler can be instructed to generate a `private static` field using the `@JvmStatic` +annotation in Kotlin. If you want the Kotlin compiler to generate a `public static` field, +you can use the `@JvmField` annotation instead. The following example is a version of the `WebServerDemo` from the previous section that has been ported to Kotlin. diff --git a/documentation/src/test/kotlin/example/registration/KotlinWebServerDemo.kt b/documentation/src/test/kotlin/example/registration/KotlinWebServerDemo.kt index 84b0d2a7021..6448a2baedf 100644 --- a/documentation/src/test/kotlin/example/registration/KotlinWebServerDemo.kt +++ b/documentation/src/test/kotlin/example/registration/KotlinWebServerDemo.kt @@ -17,7 +17,7 @@ import org.junit.jupiter.api.extension.RegisterExtension class KotlinWebServerDemo { companion object { - @JvmField + @JvmStatic @RegisterExtension val server = WebServerExtension.builder() .enableSecurity(false) diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtendWith.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtendWith.java index ed1d5764ef2..ced0f065fd1 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtendWith.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtendWith.java @@ -25,13 +25,46 @@ /** * {@code @ExtendWith} is a {@linkplain Repeatable repeatable} annotation * that is used to register {@linkplain Extension extensions} for the annotated - * test class, test interface, test method, field, or parameter. + * test class, test interface, test method, parameter, or field. * *

    Annotated parameters are supported in test class constructors, in test * methods, and in {@code @BeforeAll}, {@code @AfterAll}, {@code @BeforeEach}, * and {@code @AfterEach} lifecycle methods. * + *

    {@code @ExtendWith} fields may be either {@code static} or non-static. + * + *

    Inheritance

    + * + *

    {@code @ExtendWith} fields are inherited from superclasses as long as they + * are not hidden or overridden. Furthermore, {@code @ExtendWith} + * fields from superclasses will be registered before {@code @ExtendWith} fields + * in subclasses. + * + *

    Registration Order

    + * + *

    When {@code @ExtendWith} is present on a test class, test interface, or + * test method or on a parameter in a test method or lifecycle method, the + * corresponding extensions will be registered in the order in which the + * {@code @ExtendWith} annotations are discovered. For example, if a test class + * is annotated with {@code @ExtendWith(A.class)} and then with + * {@code @ExtendWith(B.class)}, extension {@code A} will be registered before + * extension {@code B}. + * + *

    By default, if multiple extensions are registered on fields via + * {@code @ExtendWith}, they will be ordered using an algorithm that is + * deterministic but intentionally nonobvious. This ensures that subsequent runs + * of a test suite execute extensions in the same order, thereby allowing for + * repeatable builds. However, there are times when extensions need to be + * registered in an explicit order. To achieve that, you can annotate + * {@code @ExtendWith} fields with {@link org.junit.jupiter.api.Order @Order}. + * Any {@code @ExtendWith} field not annotated with {@code @Order} will be + * ordered using the {@link org.junit.jupiter.api.Order#DEFAULT default} order + * value. Note that {@code @RegisterExtension} fields can also be ordered with + * {@code @Order}, relative to {@code @ExtendWith} fields and other + * {@code @RegisterExtension} fields. + * *

    Supported Extension APIs

    + * *
      *
    • {@link ExecutionCondition}
    • *
    • {@link InvocationInterceptor}
    • diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/RegisterExtension.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/RegisterExtension.java index 55977b63b7c..16837b7c77e 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/RegisterExtension.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/RegisterExtension.java @@ -81,7 +81,9 @@ * {@code @RegisterExtension} fields with {@link org.junit.jupiter.api.Order @Order}. * Any {@code @RegisterExtension} field not annotated with {@code @Order} will be * ordered using the {@link org.junit.jupiter.api.Order#DEFAULT default} order - * value. + * value. Note that {@code @ExtendWith} fields can also be ordered with + * {@code @Order}, relative to {@code @RegisterExtension} fields and other + * {@code @ExtendWith} fields. * *

      Example Usage

      * diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java index 9e73e91b6eb..ad6a8e4086c 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java @@ -76,8 +76,8 @@ static MutableExtensionRegistry populateNewExtensionRegistryFromExtendWithAnnota /** * Register extensions using the supplied registrar from fields in the supplied - * class that are meta-annotated with {@link ExtendWith @ExtendWith} or - * annotated with {@link RegisterExtension @RegisterExtension}. + * class that are annotated with {@link ExtendWith @ExtendWith} or + * {@link RegisterExtension @RegisterExtension}. * *

      The extensions will be sorted according to {@link Order @Order} semantics * prior to registration. @@ -141,7 +141,7 @@ static void registerExtensionsFromConstructorParameters(ExtensionRegistrar regis /** * Register extensions using the supplied registrar from parameters in the * supplied {@link Executable} (i.e., a {@link java.lang.reflect.Constructor} - * or {@link java.lang.reflect.Method}) that are meta-annotated with + * or {@link java.lang.reflect.Method}) that are annotated with * {@link ExtendWith @ExtendWith}. * * @param registrar the registrar with which to register the extensions; never {@code null}