Skip to content

Commit

Permalink
Add support in Jupiter to configure via Annotations
Browse files Browse the repository at this point in the history
This adds the @matstest annotation, as well as annotations for creating
test endpoints, and to register annotated classes. This is an
alternative way to configure tests in Jupiter for Mats.

The tests also found that if MatsFactory or MatsFuturizer is a field, it
causes conflict with Spring. Thus AbstractMatsAnnotatedClass was changed
to ignore MatsFactory and MatsFuturizer, as those should not be added to
the Spring context via the fields on the test instance.
  • Loading branch information
staale committed Feb 6, 2025
1 parent 650af5b commit 5036830
Show file tree
Hide file tree
Showing 11 changed files with 677 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package io.mats3.test.jupiter.annotation;

import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.Extension;
import org.junit.jupiter.api.extension.ExtensionContext;

import io.mats3.serial.MatsSerializer;
import io.mats3.test.jupiter.Extension_Mats;
import io.mats3.test.jupiter.annotation.MatsTest.SerializerFactory;

/**
* Extension to register the {@link io.mats3.test.jupiter.Extension_MatsEndpoint} via annotations.
* <p>
* Since the normal {@link Extension_Mats} does not have a no-args constructor, this extension is instead
* used to register the {@link Extension_Mats} extension into a test context.
*/
class Extension_MatsRegistration implements Extension, BeforeAllCallback, AfterAllCallback {

static final String LOG_PREFIX = "#MATSTEST:ANNOTATED# ";

@Override
public void beforeAll(ExtensionContext context) throws ReflectiveOperationException {
MatsTest matsTest = context.getRequiredTestClass().getAnnotation(MatsTest.class);
SerializerFactory serializerFactory = matsTest.serializerFactory().getDeclaredConstructor().newInstance();
MatsSerializer<?> matsSerializer = serializerFactory.createSerializer();

Extension_Mats extensionMats = matsTest.includeDatabase()
? Extension_Mats.createWithDb(matsSerializer)
: Extension_Mats.create(matsSerializer);
extensionMats.beforeAll(context);
}

@Override
public void afterAll(ExtensionContext context) {
Extension_Mats extensionMats = Extension_Mats.getExtension(context);
extensionMats.afterAll(context);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package io.mats3.test.jupiter.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.junit.jupiter.api.extension.ExtendWith;

import io.mats3.serial.MatsSerializer;
import io.mats3.serial.json.MatsSerializerJson;

/**
* Annotation to provide {@link io.mats3.MatsFactory} and {@link io.mats3.util.MatsFuturizer} instances for testing.
* <p>
* This annotation will allow injection of {@link io.mats3.MatsFactory} and {@link io.mats3.util.MatsFuturizer}
* instances into test constructors and test methods. This omits the need to register the
* {@link io.mats3.test.jupiter.Extension_Mats} extension. Optionally, the annotation can be used to create
* {@link io.mats3.test.jupiter.Extension_Mats} with a {@link io.mats3.serial.MatsSerializer} other than the default,
* and to also set if MATS should be set up with a database or not.
* <p>
* The annotation {@link MatsTest.Endpoint} can be applied to a field of type
* {@link io.mats3.test.jupiter.Extension_MatsEndpoint} to inject a test endpoint into the test class. Thus, there
* is no need to initialize the field, as this extension will take care of resolving relevant types, and setting
* the field before the test executes. This happens after the constructor, but before any test
* and {@link org.junit.jupiter.api.BeforeEach} methods.
* <p>
* The annotation {@link MatsTest.AnnotatedClass} can be applied to a field that has Mats annotations, like
* MatsMapping or MatsClassMapping. Similar to {@link io.mats3.test.jupiter.Extension_MatsAnnotatedClass}, these
* endpoints will be registered before the test executes. If the field is null, the class will be registered, and
* instantiated by the extension, as if you called
* {@link io.mats3.test.jupiter.Extension_MatsAnnotatedClass#withAnnotatedMatsClasses(Class[])}. However, if the field
* is already instantiated, the instance will be registered, as if you called {@link
* io.mats3.test.jupiter.Extension_MatsAnnotatedClass#withAnnotatedMatsInstances(Object[])}. This happens after the
* constructor and field initialization, but before any test and {@link org.junit.jupiter.api.BeforeEach} methods.
* <p>
* If the test class uses mockito, and @InjectMocks is used, then this becomes sensitive to the order of the
* annotations. The MockitoExtension should be placed before the MatsTest annotation, so that it can create
* instances of the annotated classes before MatsTest inspects them.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@ExtendWith({
Extension_MatsRegistration.class,
ParameterResolver_MatsFactory.class,
ParameterResolver_MatsFuturizer.class,
PostProcessor_MatsEndpoint.class,
PostProcessor_MatsAnnotatedClass.class
})
public @interface MatsTest {

/**
* Should we create the {@link io.mats3.test.jupiter.Extension_Mats} with a database or not.
* Default is no database.
* @return if the {@link io.mats3.test.jupiter.Extension_Mats} should be created with a database.
*/
boolean includeDatabase() default false;

/**
* The serializer factory to use for the {@link io.mats3.MatsFactory} created by the extension.
*
* By default, the {@link MatsSerializerJson} is used.
*
* @return the serializer factory to use for the {@link io.mats3.MatsFactory} created by the extension.
*/
Class<? extends SerializerFactory> serializerFactory() default SerializerFactoryJson.class;

/**
* Factory interface for creating a {@link MatsSerializer} instance.
* <p>
* Note: This must have a no-args constructor.
*/
interface SerializerFactory {

MatsSerializer<?> createSerializer();
}

/**
* Default serializer factory for creating a {@link MatsSerializerJson} instance.
*/
class SerializerFactoryJson implements SerializerFactory {
@Override
public MatsSerializer<?> createSerializer() {
return MatsSerializerJson.create();
}
}

/**
* Field annotation on fields of type {@link io.mats3.test.jupiter.Extension_MatsEndpoint}.
* <p>
* Use this annotation to declare that a field should be injected with a test endpoint. The name of the
* endpoint should be provided as the value of the annotation. This must be an instance of
* {@link io.mats3.test.jupiter.Extension_MatsEndpoint}. If the field is already set, then the extension will
* not change the value.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface Endpoint {

String name();
}

/**
* Marks a field that has Mats annotations, like MatsMapping or MatsClassMapping.
* <p>
* Use this annotation to declare that a field should be registered as a Mats class. If the field is null, the
* class will be registered, and instantiated by the extension. If the field is already instantiated, the instance
* will be registered. This must be a class that has Mats annotations.
* <p>
* For further documentation, see {@link io.mats3.test.jupiter.Extension_MatsAnnotatedClass}.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface AnnotatedClass { }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.mats3.test.jupiter.annotation;

import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolver;

import io.mats3.MatsFactory;
import io.mats3.test.jupiter.Extension_Mats;

/**
* Extension to provide a {@link MatsFactory} parameter to a test method.
* <p>
* Note, this is a part of {@link MatsTest}, and should not be used directly. It requires the {@link Extension_Mats}
* to be run first.
*
* @author Ståle Undheim <[email protected]> 2025-02-06
*/
class ParameterResolver_MatsFactory implements ParameterResolver {

@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return parameterContext.getParameter().getType() == MatsFactory.class;
}

@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return Extension_Mats.getExtension(extensionContext).getMatsFactory();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.mats3.test.jupiter.annotation;

import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolver;

import io.mats3.test.jupiter.Extension_Mats;
import io.mats3.util.MatsFuturizer;

/**
* Extension to provide a {@link MatsFuturizer} parameter to a test method.
* <p>
* Note, this is a part of {@link MatsTest}, and should not be used directly. It requires the {@link Extension_Mats}
* to be run first.
*
* @author Ståle Undheim <[email protected]> 2025-02-06
*/
class ParameterResolver_MatsFuturizer implements ParameterResolver {

@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return parameterContext.getParameter().getType() == MatsFuturizer.class;
}

@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return Extension_Mats.getExtension(extensionContext).getMatsFuturizer();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package io.mats3.test.jupiter.annotation;

import static io.mats3.test.jupiter.annotation.Extension_MatsRegistration.LOG_PREFIX;

import java.lang.reflect.Field;

import org.junit.jupiter.api.extension.AfterEachCallback;
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.TestInstancePostProcessor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.mats3.test.jupiter.Extension_Mats;
import io.mats3.test.jupiter.Extension_MatsAnnotatedClass;

/**
* Extension to support the {@link MatsTest.AnnotatedClass} annotation on fields in a test class.
* <p>
* Note, this is a part of {@link MatsTest}, and should not be used directly. It requires the {@link Extension_Mats}
* to be run first.
*
* @author Ståle Undheim <[email protected]> 2025-02-06
*/
class PostProcessor_MatsAnnotatedClass implements
Extension, TestInstancePostProcessor,
BeforeEachCallback, AfterEachCallback {

private static final Logger log = LoggerFactory.getLogger(PostProcessor_MatsAnnotatedClass.class);

private Extension_MatsAnnotatedClass _matsAnnotatedClass;

@Override
public void postProcessTestInstance(Object testInstance, ExtensionContext context) throws Exception {
// Since postProcessTestInstance is called before beforeEach, this is where we need to instantiate
// the Extension_MatsAnnotatedClass.
_matsAnnotatedClass = Extension_MatsAnnotatedClass.create(Extension_Mats.getExtension(context));
Field[] declaredFields = context.getRequiredTestClass().getDeclaredFields();
for (Field declaredField : declaredFields) {
if (declaredField.isAnnotationPresent(MatsTest.AnnotatedClass.class)) {
if (!declaredField.trySetAccessible()) {
throw new IllegalStateException("Could not set accessible on field [" + declaredField + "]"
+ " in test class [" + context.getRequiredTestClass() + "]"
+ " We are not able to register the annotated class"
+ " [" + declaredField.getType() + "].");
}
Object fieldValue = declaredField.get(testInstance);
if (fieldValue == null) {
log.info(LOG_PREFIX + "Registering annotated field [" + declaredField.getName() + "]"
+ " in test class [" + context.getRequiredTestClass() + "]"
+ " without an instance as an Annotated Class.");
_matsAnnotatedClass.withAnnotatedMatsClasses(declaredField.getType());
}
else {
log.info(LOG_PREFIX + "Registering annotated field [" + declaredField.getName() + "]"
+ " in test class [" + context.getRequiredTestClass() + "]"
+ " with an instance [" + fieldValue + "] as an Annotated Instance.");
_matsAnnotatedClass.withAnnotatedMatsInstances(fieldValue);
}
}
}
}

@Override
public void beforeEach(ExtensionContext context) {
_matsAnnotatedClass.beforeEach(context);
}

@Override
public void afterEach(ExtensionContext context) {
_matsAnnotatedClass.afterEach(context);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package io.mats3.test.jupiter.annotation;

import static io.mats3.test.jupiter.annotation.Extension_MatsRegistration.LOG_PREFIX;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.List;

import org.junit.jupiter.api.extension.AfterEachCallback;
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.TestInstancePostProcessor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.mats3.test.jupiter.Extension_Mats;
import io.mats3.test.jupiter.Extension_MatsEndpoint;

/**
* Extension to support the {@link MatsTest.Endpoint} annotation on fields in a test class.
* <p>
* Note, this is a part of {@link MatsTest}, and should not be used directly. It requires the {@link Extension_Mats}
* to be run first.
*
* @author Ståle Undheim <[email protected]> 2025-02-06
*/
class PostProcessor_MatsEndpoint implements
Extension, TestInstancePostProcessor,
BeforeEachCallback, AfterEachCallback {

private static final Logger log = LoggerFactory.getLogger(PostProcessor_MatsEndpoint.class);

private final List<Extension_MatsEndpoint<?, ?>> _testEndpoints = new ArrayList<>();

@Override
public void postProcessTestInstance(Object testInstance, ExtensionContext context) throws Exception {
Field[] declaredFields = context.getRequiredTestClass().getDeclaredFields();
Extension_Mats extensionMats = Extension_Mats.getExtension(context);

for (Field declaredField : declaredFields) {
if (declaredField.isAnnotationPresent(MatsTest.Endpoint.class)) {
if (!declaredField.trySetAccessible()) {
throw new IllegalStateException("Could not set accessible on field [" + declaredField + "],"
+ " in test class [" + context.getRequiredTestClass() + "]."
+ " We are not able to inject the MatsEndpoint into this class.");
}
if (!Extension_MatsEndpoint.class.equals(declaredField.getType())) {
throw new IllegalStateException(
"Field [" + declaredField + "] in test class [" + context.getRequiredTestClass() + "]"
+ " is not of type Extension_MatsEndpoint.");
}
if (declaredField.get(testInstance) != null) {
log.debug(LOG_PREFIX + "Field [" + declaredField + "]"
+ " in test class [" + context.getRequiredTestClass() + "] is already initialized");
continue;
}

MatsTest.Endpoint endpoint = declaredField.getAnnotation(MatsTest.Endpoint.class);
Extension_MatsEndpoint<?, ?> extensionMatsEndpoint = Extension_MatsEndpoint.create(
extensionMats,
endpoint.name(),
(Class<?>) ((ParameterizedType) declaredField.getGenericType()).getActualTypeArguments()[0],
(Class<?>) ((ParameterizedType) declaredField.getGenericType()).getActualTypeArguments()[1]
);
log.info(LOG_PREFIX + "Injecting MatsEndpoint [" + endpoint.name() + "]"
+ " into field [" + declaredField + "]"
+ " in test class [" + context.getRequiredTestClass() + "].");
_testEndpoints.add(extensionMatsEndpoint);
declaredField.set(testInstance, extensionMatsEndpoint);
}
else {
log.debug(LOG_PREFIX + "Field [" + declaredField + "] in test class [" + context.getRequiredTestClass()
+ "] is not annotated with @MatsTestEndpoint, so it will not be injected.");
}
}
}

@Override
public void beforeEach(ExtensionContext context) {
_testEndpoints.forEach(extensionMatsEndpoint -> extensionMatsEndpoint.beforeEach(context));
}

@Override
public void afterEach(ExtensionContext context) {
_testEndpoints.forEach(extensionMatsEndpoint -> extensionMatsEndpoint.afterEach(context));
}
}
Loading

0 comments on commit 5036830

Please sign in to comment.