Skip to content

Commit

Permalink
Field Injection (#8)
Browse files Browse the repository at this point in the history
[minor]
  • Loading branch information
goughy000 authored Dec 9, 2023
1 parent efda27b commit c1d2e38
Show file tree
Hide file tree
Showing 11 changed files with 282 additions and 66 deletions.
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,11 @@ jump straight to [Injecting resources](#injecting-resources)

## Injecting resources

Resources can be injected into tests through parameters on test methods. A number of different types can be used
which are [listed here](#supported-types). The parameter which needs to be populated must be annotated with the
`@TestResource` annotation, which configures the path to the resource
Resources can be injected into tests through parameters on test methods, or non-static fields in test classes. A number
of different types can be used which are [listed here](#supported-types). The parameter which needs to be populated must be
annotated with the `@TestResource` annotation, which configures the path to the resource

```java
@ExtendWith(ResourcesExtension.class)
class MyTests {
@Test
void myTest(@TestResource("myFile.txt") String myFile) {
Expand All @@ -48,6 +47,12 @@ class MyTests {
void myOtherTest(@TestResource("myFile.txt") InputStream inputStream) {
// `inputStream` is reading the contents of myFile.txt
}

@TestResource("myFile.txt") String field;
@Test
void testFromField() {
// `field` will contain the contents of myFile.txt
}
}
```

Expand All @@ -57,7 +62,6 @@ For some types, the charset can make a difference and so this can also be specif
By default, the system default charset will be used.

```java
@ExtendWith(ResourcesExtension.class)
class MyTests {
@Test
void myTest(@TestResource(value = "myFile.txt", charset = "UTF-8") String myFile) {
Expand All @@ -79,12 +83,12 @@ You can use a `/` at the start of your filename to read from the root of the cla
`@TestResource("/file.txt")`.

You can also make use of the `@TestResourceDirectory` annotation to set a default directory to save specifying it
on every parameter. This annotation can be specified on a test class, method or parameter.
on every parameter. This annotation can be specified on a test field, parameter, method, class, parent class or package
(in `package-info.java`). The first annotation found in this order will be used to determine the directory.

```java
package com.example;

@ExtendWith(ResourcesExtension.class)
@TestResourceDirectory("subdir")
class SomeTests {
@Test
Expand Down
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ plugins {
id 'maven-publish'
id 'signing'
id 'io.toolebox.git-versioner' version '1.6.5'
id 'com.diffplug.spotless' version '6.22.0'
id 'com.diffplug.spotless' version '6.23.3'
}

group 'com.testingsyndicate'
Expand Down Expand Up @@ -43,7 +43,7 @@ spotless {
endWithNewline()
}
groovyGradle {
greclipse('4.27')
greclipse()
indentWithSpaces(2)
endWithNewline()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.testingsyndicate.jupiter.extensions.resources;

import static org.junit.platform.commons.support.AnnotationSupport.findAnnotation;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Parameter;
import java.util.List;
import org.junit.platform.commons.support.AnnotationSupport;
import org.junit.platform.commons.support.SearchOption;

class Annotations {
private Annotations() {}

static <T extends Annotation> T findHierarchical(AnnotatedElement element, Class<T> annotation) {
var result =
element instanceof Class<?> clazz
? findAnnotation(clazz, annotation, SearchOption.INCLUDE_ENCLOSING_CLASSES)
: findAnnotation(element, annotation);

if (result.isPresent()) {
return result.get();
}

if (element instanceof Parameter parameter) {
return findHierarchical(parameter.getDeclaringExecutable(), annotation);
}

if (element instanceof Member member) {
return findHierarchical(member.getDeclaringClass(), annotation);
}

if (element instanceof Class<?> clazz) {
return findHierarchical(clazz.getPackage(), annotation);
}

return null;
}

static <T extends Annotation> List<AnnotatedField<T>> findAnnotatedFields(
Class<?> clazz, Class<T> annotation) {
return AnnotationSupport.findAnnotatedFields(clazz, annotation).stream()
.map(f -> new AnnotatedField<>(f, f.getAnnotation(annotation)))
.toList();
}

record AnnotatedField<T extends Annotation>(Field field, T annotation) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.testingsyndicate.jupiter.extensions.resources;

class NameBuilder {
private static final String SEPARATOR = "/";
private final StringBuilder name = new StringBuilder();

NameBuilder() {}

public NameBuilder append(String part) {
if (part == null) {
return this;
}
if (part.startsWith(SEPARATOR) || name.isEmpty()) {
name.replace(0, name.length(), part);
return this;
}
if (!SEPARATOR.equals(name.substring(name.length() - SEPARATOR.length()))) {
name.append(SEPARATOR);
}
name.append(part);

return this;
}

public String build() {
return name.toString();
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package com.testingsyndicate.jupiter.extensions.resources;

import static org.junit.platform.commons.support.AnnotationSupport.findAnnotation;

import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
import java.util.Optional;
import org.junit.jupiter.api.extension.*;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.Extension;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import org.junit.platform.commons.support.SearchOption;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;

/**
* Jupiter extension to resolve resources into parameters in tests
Expand All @@ -22,8 +23,7 @@
* }
* </code> </pre>
*/
public class ResourcesExtension implements Extension, ParameterResolver {
private static final String SEPARATOR = "/";
public class ResourcesExtension implements Extension, ParameterResolver, BeforeEachCallback {
private static final Namespace NAMESPACE = Namespace.create(ResourcesExtension.class);

@Override
Expand All @@ -36,21 +36,50 @@ public boolean supportsParameter(ParameterContext parameter, ExtensionContext ex
public Object resolveParameter(ParameterContext context, ExtensionContext extension)
throws ParameterResolutionException {

var annotation = findAnnotation(context.getParameter(), TestResource.class).orElseThrow();
var path = fullPath(context, annotation.value());
var clazz = findDeclaration(context);
var parameter = context.getParameter();
var annotation = parameter.getAnnotation(TestResource.class);
var directory = Annotations.findHierarchical(parameter, TestResourceDirectory.class);
var clazz = parameter.getDeclaringExecutable().getDeclaringClass();

return resolveResource(extension, annotation, directory, parameter.getType(), clazz);
}

@Override
public void beforeEach(ExtensionContext context) throws Exception {
var clazz = context.getRequiredTestClass();
var instance = context.getRequiredTestInstance();
for (var result : Annotations.findAnnotatedFields(clazz, TestResource.class)) {
var field = result.field();
var directory = Annotations.findHierarchical(field, TestResourceDirectory.class);
var type = field.getType();
var value = resolveResource(context, result.annotation(), directory, type, clazz);
field.setAccessible(true);
field.set(instance, value);
}
}

var resolver = findResolver(context);
var url = clazz.getResource(path);
private static Object resolveResource(
ExtensionContext context,
TestResource annotation,
TestResourceDirectory directory,
Class<?> target,
Class<?> clazz) {
var resolver = findResolver(target);
var name =
new NameBuilder()
.append(directory == null ? null : directory.value())
.append(annotation.value())
.build();
var url = clazz.getResource(name);

if (url == null) {
throw new ParameterResolutionException("Unable to find resource %s".formatted(path));
throw new ParameterResolutionException("Unable to find resource %s".formatted(name));
}

var charset = resolveCharset(annotation.charset());

var resource = resolver.resolve(url, charset);
registerResource(extension, resource);
registerResource(context, resource);
return resource;
}

Expand All @@ -65,56 +94,17 @@ private static Charset resolveCharset(String name) {
}
}

private static Optional<TestResourceDirectory> findDirectory(ParameterContext context) {
var annotation = findAnnotation(context.getParameter(), TestResourceDirectory.class);
if (annotation.isPresent()) {
return annotation;
}

var executable = context.getDeclaringExecutable();
annotation = findAnnotation(executable, TestResourceDirectory.class);
if (annotation.isPresent()) {
return annotation;
}

var clazz = executable.getDeclaringClass();
return findAnnotation(
clazz, TestResourceDirectory.class, SearchOption.INCLUDE_ENCLOSING_CLASSES);
}

private static ResourceResolver<?> findResolver(ParameterContext context) {
var parameter = context.getParameter();
var target = parameter.getType();
private static ResourceResolver<?> findResolver(Class<?> target) {
return ResourceResolver.getResolver(target)
.orElseThrow(
() ->
new ParameterResolutionException(
"No resolver registered for type " + target.getTypeName()));
}

private static String fullPath(ParameterContext context, String name) {
var directory = findDirectory(context);
if (name.startsWith(SEPARATOR) || directory.isEmpty()) {
return name;
}

var path = directory.get().value();
var builder = new StringBuilder(path);
if (!path.endsWith(SEPARATOR)) {
builder.append(SEPARATOR);
}
builder.append(name);

return builder.toString();
}

private static void registerResource(ExtensionContext context, Object resource) {
if (resource instanceof AutoCloseable closeable) {
context.getStore(NAMESPACE).put(resource, new QuietCloseable(closeable));
}
}

private static Class<?> findDeclaration(ParameterContext context) {
return context.getParameter().getDeclaringExecutable().getDeclaringClass();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

/** Specifies the options for injecting a resource into a test parameter. */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@Target({ElementType.PARAMETER, ElementType.FIELD})
@ExtendWith(ResourcesExtension.class)
public @interface TestResource {
/** Name of the file of the resource to be injected */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@

/** Specifies a default directory from which to load test resources */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.PARAMETER, ElementType.METHOD})
@Target({
ElementType.TYPE,
ElementType.PARAMETER,
ElementType.METHOD,
ElementType.FIELD,
ElementType.PACKAGE
})
public @interface TestResourceDirectory {
/** Directory name to set as the default */
String value();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.testingsyndicate.jupiter.extensions.resources;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.params.provider.Arguments.arguments;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.AnnotatedElement;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

class AnnotationsTest {

@ParameterizedTest
@MethodSource
void findsAnnotationHierarchically(AnnotatedElement element, String expected) {
// when
var actual = Annotations.findHierarchical(element, FindMe.class);

// then
var value = actual == null ? null : actual.value();
assertThat(value).isEqualTo(expected);
}

static Stream<Arguments> findsAnnotationHierarchically() throws Exception {
var clazz = AnnotatedClass.class;
var annotatedMethod = clazz.getDeclaredMethod("annotatedMethod", String.class, String.class);
var unannotatedMethod = clazz.getDeclaredMethod("unannotatedMethod");
return Stream.of(
arguments(clazz, "class"),
arguments(clazz.getDeclaredField("annotatedField"), "field"),
arguments(clazz.getDeclaredField("unannotatedField"), "class"),
arguments(annotatedMethod, "method"),
arguments(unannotatedMethod, "class"),
arguments(annotatedMethod.getParameters()[0], "parameter"),
arguments(annotatedMethod.getParameters()[1], "method"),
arguments(UnannotatedClass.class, "package"));
}

@Retention(RetentionPolicy.RUNTIME)
@interface FindMe {
String value();
}

@FindMe("class")
private static class AnnotatedClass {

@FindMe("field")
private String annotatedField;

private String unannotatedField;

@FindMe("method")
private void annotatedMethod(
@FindMe("parameter") String annotatedParameter, String unannotatedParameter) {}

private void unannotatedMethod() {}
}

private static class UnannotatedClass {}
}
Loading

0 comments on commit c1d2e38

Please sign in to comment.