Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue736 unit testing w prop files take2 #742

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Base class can now auto-find annotation properties.
Fixed dependency on recent (only) versions of Junit
  • Loading branch information
ee-usgs committed Nov 12, 2022
commit 2e2eb8259d9e6e403a8f15a6b36425ab5a606e29
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@

import org.junit.jupiter.api.extension.*;
import org.junit.platform.commons.support.AnnotationSupport;
import org.junit.platform.commons.util.Preconditions;
import org.yarnandtail.andhow.*;
import org.yarnandtail.andhow.api.StandardLoader;
import org.yarnandtail.andhow.load.std.*;
import org.yarnandtail.andhow.testutil.AndHowTestUtils;
import sun.reflect.annotation.AnnotationType;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Modifier;
import java.lang.reflect.*;
import java.util.*;

import static org.junit.platform.commons.support.SearchOption.INCLUDE_ENCLOSING_CLASSES;
import static org.junit.platform.commons.util.ReflectionUtils.isInnerClass;

/**
* Implementation of an ExtensionBase that configures AndHow from a single properties
Expand Down Expand Up @@ -104,17 +106,62 @@ public ConfigFromFileBaseExt() { }

/**
* When this Extension is being used in association w/ an annotation, this returns
* that class.
* the filePath from the annotation.
*
* @param context
* @return
*/
protected abstract String getFilePathFromAnnotation(ExtensionContext context);
protected String getFilePathFromAnnotation(ExtensionContext context) {
Annotation a = findAnnotation(getAssociatedAnnotation(), context);
Method method = getClasspathFileMethod();
method.setAccessible(true);
try {
return (String)(method.invoke(a));
} catch (Exception e) {
throw new RuntimeException("Unable to use reflection to access annotation values.", e);
}
}

/**
* When this Extension is being used in association w/ an annotation, this returns
* the optional configClasses List.
* the includedClasses from the annotation.
*
* @return
*/
protected Class<?>[] getClassesInScopeFromAnnotation(ExtensionContext context) {
Annotation a = findAnnotation(getAssociatedAnnotation(), context);
Method method = getIncludeClassesMethod();
method.setAccessible(true);
try {
return (Class<?>[])(method.invoke(a));
} catch (Exception e) {
throw new RuntimeException("Unable to use reflection to access annotation values.", e);
}
}

/**
* The annotation associated with the implementation.
* <p>
* See existing annotations w/ similar names for example, e.g. ConfigFromFileBeforeAllTests.
* The annotation must have two property methods:
* <ul>
* <li>String value: The classpath to the property file to use for configuration</li>
* <li>Class<?>[] includeClasses: Array of classes that are in scope for AndHow</li>
* </ul>
* The actual names of the methods can be overridden by overriding getClasspathFileMethod() and/or
* getIncludeClassesMethod().
*
* @return
*/
protected abstract Class<?>[] getClassesInScopeFromAnnotation(ExtensionContext context);
public abstract Class<? extends Annotation> getAssociatedAnnotation();

public Method getClasspathFileMethod() {
return AnnotationType.getInstance(getAssociatedAnnotation()).members().get("value");
}

public Method getIncludeClassesMethod() {
return AnnotationType.getInstance(getAssociatedAnnotation()).members().get("includeClasses");
}


//
Expand Down Expand Up @@ -167,45 +214,79 @@ public void afterAllOrEach(ExtensionContext context) throws Exception {

protected <A extends Annotation> A findAnnotation(Class<A> annotationClass, ExtensionContext context) {

Optional<A> ann = null;
Optional<AnnotatedElement> ae = context.getElement();

switch (getExtensionType().getScope()) {
case TEST_CLASS:
case EACH_TEST:
// In both cases, the associated annotation should be at the class level
case EACH_TEST: {
// In both cases, the associated annotation should be at the class level

//This method will hunt nested classes, and because the AndHow annotations are marked for
//inheritance, the superclass ones as well.
ann = AnnotationSupport.findAnnotation(
context.getRequiredTestClass(), annotationClass, INCLUDE_ENCLOSING_CLASSES);

if (! ann.isPresent()) {
throw new IllegalStateException("Expected the @" + annotationClass.getName() + " annotation on the '" +
context.getRequiredTestClass() + "' class, superclass or a parent class for a @Nested test.");
}
//This method will hunt nested classes, and because the AndHow annotations are marked for
//inheritance, the superclass ones as well.

//Note: Could use the JUnit AnnotationSupport.findAnnotation(Class<?>, Class<?>, SearchOption)
//method here, but its experimental and only added in recent versions of Junit.
Optional<A> ann = findAnnotation(context.getRequiredTestClass(), annotationClass);

break;
case SINGLE_TEST:

// Operating at the method level, so the annotation should be in the current test method
ann = AnnotationSupport.findAnnotation(context.getElement(), annotationClass);
if (!ann.isPresent()) {
throw new IllegalStateException("Expected the @" + annotationClass.getName() + " annotation on the '" +
context.getRequiredTestClass() + "' class, superclass or a parent class for a @Nested test.");
}

if (! ann.isPresent()) {
throw new IllegalStateException("Expected the @" + annotationClass.getName() + " annotation on the '" +
context.getRequiredTestMethod().getName() + "' test method of " + context.getRequiredTestClass());
return ann.get();
}
case SINGLE_TEST: {
// Operating at the method level, so the annotation should be in the current test method
Optional<A> ann = AnnotationSupport.findAnnotation(context.getElement(), annotationClass);

break;
if (!ann.isPresent()) {
throw new IllegalStateException("Expected the @" + annotationClass.getName() + " annotation on the '" +
context.getRequiredTestMethod().getName() + "' test method of " + context.getRequiredTestClass());
}

return ann.get();
}
default:
throw new IllegalStateException("Cannot call findAnnotation() if the getExtensionType() returns " +
"a type that doesn't use TEST_CLASS, EACH_TEST or SINGLE_TEST scope.");
}
}

return ann.get();
/**
* Copied from JUnit AnnotationUtils
*
* ref: AnnotationUtils.findAnnotation(Class<?> clazz, Class<A> annotationType,
* boolean searchEnclosingClasses)
* @param clazz
* @param annotationType
* @return
* @param <A>
*/
public static <A extends Annotation> Optional<A> findAnnotation(Class<?> clazz, Class<A> annotationType) {

Class<?> candidate = clazz;
while (candidate != null) {
Optional<A> annotation = AnnotationSupport.findAnnotation(candidate, annotationType);
if (annotation.isPresent()) {
return annotation;
}
candidate = (isInnerClass(candidate) ? candidate.getEnclosingClass() : null);
}
return Optional.empty();
}

/**
* Is this a non-static innerClass?
*
* @param clazz
* @return
*/
public static boolean isInnerClass(Class<?> clazz) {
return !(Modifier.isStatic(clazz.getModifiers())) && clazz.isMemberClass();
}


/**
* Find the user configured properties file path.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import org.junit.jupiter.api.extension.*;
import org.yarnandtail.andhow.junit5.ConfigFromFileBeforeAllTests;

import java.lang.annotation.Annotation;

public class ConfigFromFileBeforeAllTestsExt extends ConfigFromFileBaseExt
implements BeforeAllCallback, AfterAllCallback {

Expand All @@ -29,28 +31,11 @@ public ExtensionType getExtensionType() {
return ExtensionType.CONFIG_ALL_TESTS;
}

/**
* Find the annotated filePath property in the @ConfigFromFileBeforeAllTests annotation on the
* test class.
* <p>
* In the case of a @Nested test, the ConfigFromFileBeforeAllTestsExt is invoked again for each
* nested test class so the code recursively hunts up the inheritance chain to find the class
* with the annotation.
*
* @param context
* @return
*/
@Override
protected String getFilePathFromAnnotation(ExtensionContext context) {
return findAnnotation(ConfigFromFileBeforeAllTests.class, context).value();
}

@Override
protected Class<?>[] getClassesInScopeFromAnnotation(ExtensionContext context) {
return findAnnotation(ConfigFromFileBeforeAllTests.class, context).includeClasses();
public Class<? extends Annotation> getAssociatedAnnotation() {
return ConfigFromFileBeforeAllTests.class;
}


@Override
public void beforeAll(final ExtensionContext context) throws Exception {
super.beforeAllOrEach(context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import org.yarnandtail.andhow.junit5.ConfigFromFileBeforeAllTests;
import org.yarnandtail.andhow.junit5.ConfigFromFileBeforeEachTest;

import java.lang.annotation.Annotation;

public class ConfigFromFileBeforeEachTestExt extends ConfigFromFileBaseExt
implements BeforeEachCallback, AfterEachCallback {

Expand All @@ -30,25 +32,9 @@ public ExtensionType getExtensionType() {
return ExtensionType.CONFIG_EACH_TEST;
}

/**
* Find the annotated filePath property in the @ConfigFromFileBeforeEachTest annotation on the
* test class.
* <p>
* In the case of a @Nested test, the ConfigFromFileBeforeEachTestExt is invoked again for each
* nested test class so the code recursively hunts up the inheritance chain to find the class
* with the annotation.
*
* @param context
* @return
*/
@Override
protected String getFilePathFromAnnotation(ExtensionContext context) {
return findAnnotation(ConfigFromFileBeforeEachTest.class, context).value();
}

@Override
protected Class<?>[] getClassesInScopeFromAnnotation(ExtensionContext context) {
return findAnnotation(ConfigFromFileBeforeEachTest.class, context).includeClasses();
public Class<? extends Annotation> getAssociatedAnnotation() {
return ConfigFromFileBeforeEachTest.class;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package org.yarnandtail.andhow.junit5.ext;

import org.junit.jupiter.api.extension.*;
import org.yarnandtail.andhow.junit5.ConfigFromFileBeforeAllTests;
import org.yarnandtail.andhow.junit5.ConfigFromFileBeforeThisTest;
import org.yarnandtail.andhow.junit5.*;
import sun.reflect.annotation.AnnotationType;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ConfigFromFileBeforeThisTestExt extends ConfigFromFileBaseExt
implements BeforeEachCallback, AfterEachCallback {
Expand Down Expand Up @@ -30,22 +34,8 @@ public ExtensionType getExtensionType() {
return ExtensionType.CONFIG_THIS_TEST;
}

/**
* Find the annotated filePath property in the @ConfigFromFileBeforeThisTest annotation on the
* test class.
* <p>
*
* @param context
* @return
*/
@Override
protected String getFilePathFromAnnotation(ExtensionContext context) {
return findAnnotation(ConfigFromFileBeforeThisTest.class, context).value();
}

@Override
protected Class<?>[] getClassesInScopeFromAnnotation(ExtensionContext context) {
return findAnnotation(ConfigFromFileBeforeThisTest.class, context).includeClasses();
public Class<? extends Annotation> getAssociatedAnnotation() {
return ConfigFromFileBeforeThisTest.class;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.mockito.*;
import org.yarnandtail.andhow.junit5.ConfigFromFileBeforeThisTest;
import org.yarnandtail.andhow.junit5.ext.ConfigFromFileBaseExt;
import org.yarnandtail.andhow.junit5.ext.ExtensionType;
import org.yarnandtail.andhow.testutil.AndHowTestUtils;

import java.lang.annotation.Annotation;

import static org.junit.jupiter.api.Assertions.*;

/**
Expand Down Expand Up @@ -106,6 +109,11 @@ protected Class<?>[] getClassesInScopeFromAnnotation(final ExtensionContext cont
return new Class[0];
}

@Override
public Class<? extends Annotation> getAssociatedAnnotation() {
return ConfigFromFileBeforeThisTest.class;
}

public String expandPath(String classpath, ExtensionContext context) {
return super.expandPath(classpath, context);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
import org.junit.jupiter.api.extension.ExtensionContext;
import org.mockito.*;
import org.yarnandtail.andhow.*;
import org.yarnandtail.andhow.junit5.ConfigFromFileBeforeAllTests;
import org.yarnandtail.andhow.junit5.ConfigFromFileBeforeThisTest;
import org.yarnandtail.andhow.testutil.AndHowTestUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;

import static org.junit.jupiter.api.Assertions.*;
Expand Down Expand Up @@ -320,6 +323,11 @@ protected Class<?>[] getClassesInScopeFromAnnotation(final ExtensionContext cont
return new Class[0];
}

@Override
public Class<? extends Annotation> getAssociatedAnnotation() {
return ConfigFromFileBeforeAllTests.class;
}

public static String getCoreKey() {
return ConfigFromFileBaseExt.CORE_KEY;
}
Expand Down