I am familiar with composite annotations. However, it seems they are not enough for my specific need even after some research.
General case
I want to create an annotation for testing that, put on a class along with some attributes, can customize the test context.
public @interface MyTestingAnnotation {
String myTestingAttribute()
}
The value for myTestingAttribute should be read by one of my custom (TBD) @TestConfigurations
Practical example
In my applications, I must mock the clock so that the test can run simulating a specific point of time. E.g. the result of the test does not depend on the hardware clock. I define a java.time.Clock bean for the purpose.
Currently, I only have an annotation to enable the mock clock, but specifying its time depends on @TestPropertySource
/**
* Provides mocking configuration for the {@link Clock} bean
* <p></p>
* Injected in all relevant Spring beans that require time access, can be set to a fixed time in order to provide repeatable
* tests
* In general, time zone is assumed to be UTC in all cases.
* <p></p>
* This bean reads the property <pre>clock.fixedInstant</pre> to determine the current time that is being mocked to all beans
* By default, it is a known point in time: <pre>4 November 2023 at 15:45:32 UTC</pre>
* Individual test can override the fixed time by using
* <pre>@TestPropertySource(properties = "clock.fixed-instant:[... ISO 8601 ...]")</pre>
*/
@TestConfiguration
public class MockClockConfiguration {
@Value("${clock.fixed-instant:2023-11-04T15:45:32.000000Z}")
private OffsetDateTime clockFixedInstant;
private static final ThreadLocal<OffsetDateTime> fixedInstantLocal = new ThreadLocal<>();
/**
* Convenience method for tests to retrieve the fixed time
* @return
*/
public static OffsetDateTime getFixedInstant(){
return fixedInstantLocal.get();
}
@Primary
@Bean
public Clock fixedClock() {
fixedInstantLocal.set(clockFixedInstant);
return Clock.fixed(Instant.from(clockFixedInstant), clockFixedInstant.getOffset());
}
}
Instead, I would like to annotate a test like @MockClock(at = "2024-02-15T12:35:00+04:00") and not necessarily use the Property source syntax.
I know how to use @AliasFor in my custom annotations, but currently I can only @Import(MockClockConfiguration.class) in my meta-annotations.
How can I achieve something like that in Spring?
You can use a
ContextCustomizerfor this purpose.This approach allows you to modify the application context before tests are run, providing a fine-grained control over the context configuration, including adding or overriding beans, properties, and more.
To achieve your goal with a
ContextCustomizer, follow these steps:ContextCustomizerCreate the
ContextCustomizerthat will modify the application context's environment or bean definitions based on the@MockClockannotation.ContextCustomizerFactoryCreate a
ContextCustomizerFactorythat looks for your@MockClockannotation on the test class and produces aContextCustomizerbased on the annotation's attributes.ContextCustomizerFactorySpring does not automatically discover ContextCustomizerFactory implementations. To register your custom factory, you need to create a file named
META-INF/spring.factoriesin the resources directory of your project (if it doesn't already exist) and add the following line:go
Replace
your.packagewith the actual package name where yourMockClockContextCustomizerFactoryis located.As an alternative, you can register a
ContextCustomizerFactorylocally via@ContextCustomizerFactoriessince Spring Framework 6.1.How It Works
@MockClockis executed, Spring Test looks forContextCustomizerFactoryimplementations specified inspring.factories.MockClockContextCustomizerFactorychecks for the presence of the@MockClockannotation and, if found, creates an instance ofMockClockContextCustomizerwith the specified fixed instant.MockClockContextCustomizerthen customizes the application context before the test runs, adding the required property to the environment, which yourMockClockConfigurationcan use to create the mocked Clock bean.