From d9e5760ea7ab79777873ee191a9f718213e46db5 Mon Sep 17 00:00:00 2001 From: Michael Buckley Date: Mon, 26 Aug 2024 09:02:44 -0400 Subject: [PATCH] New junit parameter provider from static fields. (#6232) --- .../JunitFieldParameterProviderExtension.java | 92 +++++++++++++++++++ .../ca/uhn/test/junit/JunitFieldProvider.java | 18 ++++ .../java/ca/uhn/test/junit/package-info.java | 4 + ...itFieldParameterProviderExtensionTest.java | 46 ++++++++++ 4 files changed, 160 insertions(+) create mode 100644 hapi-fhir-test-utilities/src/main/java/ca/uhn/test/junit/JunitFieldParameterProviderExtension.java create mode 100644 hapi-fhir-test-utilities/src/main/java/ca/uhn/test/junit/JunitFieldProvider.java create mode 100644 hapi-fhir-test-utilities/src/main/java/ca/uhn/test/junit/package-info.java create mode 100644 hapi-fhir-test-utilities/src/test/java/ca/uhn/test/junit/JunitFieldParameterProviderExtensionTest.java diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/test/junit/JunitFieldParameterProviderExtension.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/test/junit/JunitFieldParameterProviderExtension.java new file mode 100644 index 00000000000..481ae84e972 --- /dev/null +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/test/junit/JunitFieldParameterProviderExtension.java @@ -0,0 +1,92 @@ +package ca.uhn.test.junit; + +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +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.platform.commons.util.ExceptionUtils; +import org.junit.platform.commons.util.ReflectionUtils; + +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Predicate; + +import static org.junit.platform.commons.util.AnnotationUtils.findAnnotatedFields; +import static org.junit.platform.commons.util.ReflectionUtils.makeAccessible; + +/** + * Register any field annotated with @{@link JunitFieldProvider} as a parameter provider, matching parameters by type. + * Can also be used directly via @RegisterExtension, using values passed to the constructor. + */ +public class JunitFieldParameterProviderExtension implements BeforeAllCallback, BeforeEachCallback, ParameterResolver { + + Set myValues = new HashSet<>(); + + /** + * Junit constructor for @{@link org.junit.jupiter.api.extension.ExtendWith} + */ + public JunitFieldParameterProviderExtension() {} + + /** + * Used for explicit registration. + * @param theValues the values to register as provided junit parameters. + */ + public JunitFieldParameterProviderExtension(Object ...theValues) { + Collections.addAll(myValues, theValues); + } + + + /** + * Do we have a value matching the parameter type? + */ + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + Class paramType = parameterContext.getParameter().getType(); + return myValues.stream().anyMatch(v->paramType.isAssignableFrom(v.getClass())); + } + + /** + * Find the first value matching the parameter type. + */ + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + Class paramType = parameterContext.getParameter().getType(); + return myValues.stream().filter(v->paramType.isAssignableFrom(v.getClass())).findAny().orElseThrow(); + } + + /** + * Collect all statics annotated with @JunitFieldProvider. + */ + @Override + public void beforeAll(ExtensionContext context) throws Exception { + collectFields(null, context.getRequiredTestClass(), ReflectionUtils::isStatic); + } + + /** + * Collect any instance fields annotated with @JunitFieldProvider. + */ + @Override + public void beforeEach(ExtensionContext context) { + context.getRequiredTestInstances().getAllInstances() // + .forEach(instance -> collectFields(instance, instance.getClass(), ReflectionUtils::isStatic)); + } + + private void collectFields(Object testInstance, Class testClass, + Predicate predicate) { + + findAnnotatedFields(testClass, JunitFieldProvider.class, predicate).forEach(field -> { + try { + makeAccessible(field); + myValues.add(field.get(testInstance)); + } + catch (Exception t) { + ExceptionUtils.throwAsUncheckedException(t); + } + }); + } + +} diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/test/junit/JunitFieldProvider.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/test/junit/JunitFieldProvider.java new file mode 100644 index 00000000000..2ac5313a0a5 --- /dev/null +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/test/junit/JunitFieldProvider.java @@ -0,0 +1,18 @@ +package ca.uhn.test.junit; + +import org.junit.jupiter.api.extension.ExtendWith; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Field annotation to register a value for use in JUnit 5 parameter resolution. + */ +@Target({ElementType.ANNOTATION_TYPE, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(JunitFieldParameterProviderExtension.class) +public @interface JunitFieldProvider { + +} diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/test/junit/package-info.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/test/junit/package-info.java new file mode 100644 index 00000000000..4b262230878 --- /dev/null +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/test/junit/package-info.java @@ -0,0 +1,4 @@ +/** + * Junit helpers + */ +package ca.uhn.test.junit; diff --git a/hapi-fhir-test-utilities/src/test/java/ca/uhn/test/junit/JunitFieldParameterProviderExtensionTest.java b/hapi-fhir-test-utilities/src/test/java/ca/uhn/test/junit/JunitFieldParameterProviderExtensionTest.java new file mode 100644 index 00000000000..da99e1ae645 --- /dev/null +++ b/hapi-fhir-test-utilities/src/test/java/ca/uhn/test/junit/JunitFieldParameterProviderExtensionTest.java @@ -0,0 +1,46 @@ +package ca.uhn.test.junit; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class JunitFieldParameterProviderExtensionTest { + /** Verify statics annotated with @{@link JunitFieldProvider} are available to static junit methods. */ + @Nested + class AnnotationRegistration implements IParameterizedTestTemplate { + @JunitFieldProvider + static final CaseSupplyingFixture ourFixture = new CaseSupplyingFixture(); + } + /** Verify explicit registration of the extension.*/ + @Nested + class ExplicitRegistration implements IParameterizedTestTemplate { + static final CaseSupplyingFixture ourFixture = new CaseSupplyingFixture(); + @RegisterExtension + static final JunitFieldParameterProviderExtension ourExtension = new JunitFieldParameterProviderExtension(ourFixture); + } + + static class CaseSupplyingFixture { + public List getCases() { + return List.of(1,2,3); + } + } + interface IParameterizedTestTemplate { + @ParameterizedTest + // intellij complains when a static source requires params + @SuppressWarnings("JUnitMalformedDeclaration") + @MethodSource("testCaseSource") + default void testStaticFactoryBound(int theTestCase) { + // given + assertTrue(theTestCase > 0); + } + + static List testCaseSource(CaseSupplyingFixture theFixture) { + return theFixture.getCases(); + } + } +}