-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support in Jupiter to configure via Annotations
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
Showing
11 changed files
with
677 additions
and
0 deletions.
There are no files selected for viewing
40 changes: 40 additions & 0 deletions
40
...st-jupiter/src/main/java/io/mats3/test/jupiter/annotation/Extension_MatsRegistration.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
|
||
} |
115 changes: 115 additions & 0 deletions
115
mats-test-jupiter/src/main/java/io/mats3/test/jupiter/annotation/MatsTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 { } | ||
} |
29 changes: 29 additions & 0 deletions
29
...jupiter/src/main/java/io/mats3/test/jupiter/annotation/ParameterResolver_MatsFactory.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
29 changes: 29 additions & 0 deletions
29
...piter/src/main/java/io/mats3/test/jupiter/annotation/ParameterResolver_MatsFuturizer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
75 changes: 75 additions & 0 deletions
75
...iter/src/main/java/io/mats3/test/jupiter/annotation/PostProcessor_MatsAnnotatedClass.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
|
||
} |
89 changes: 89 additions & 0 deletions
89
...st-jupiter/src/main/java/io/mats3/test/jupiter/annotation/PostProcessor_MatsEndpoint.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
} |
Oops, something went wrong.