Add support for bean overriding in tests

This commit introduces two sets of annotations (`@TestBean` on one side
and `MockitoBean`/`MockitoSpyBean` on the other side), as well as an
extension mecanism based on the `@BeanOverride` meta-annotation.

Extension implementors are expected to only provide an annotation,
a BeanOverrideProcessor implementation and an OverrideMetadata subclass.

Closes gh-29917.
This commit is contained in:
Simon Baslé
2023-12-14 11:43:37 +01:00
parent 90867e7e62
commit e1bbdf0913
41 changed files with 3516 additions and 3 deletions

View File

@@ -183,6 +183,7 @@
***** xref:testing/annotations/integration-spring/annotation-sqlmergemode.adoc[]
***** xref:testing/annotations/integration-spring/annotation-sqlgroup.adoc[]
***** xref:testing/annotations/integration-spring/annotation-disabledinaotmode.adoc[]
***** xref:testing/annotations/integration-spring/annotation-beanoverriding.adoc[]
**** xref:testing/annotations/integration-junit4.adoc[]
**** xref:testing/annotations/integration-junit-jupiter.adoc[]
**** xref:testing/annotations/integration-meta.adoc[]

View File

@@ -28,4 +28,6 @@ Spring's testing annotations include the following:
* xref:testing/annotations/integration-spring/annotation-sqlmergemode.adoc[`@SqlMergeMode`]
* xref:testing/annotations/integration-spring/annotation-sqlgroup.adoc[`@SqlGroup`]
* xref:testing/annotations/integration-spring/annotation-disabledinaotmode.adoc[`@DisabledInAotMode`]
* xref:testing/annotations/integration-spring/annotation-beanoverriding.adoc#spring-testing-annotation-beanoverriding-testbean[`@TestBean`]
* xref:testing/annotations/integration-spring/annotation-beanoverriding.adoc#spring-testing-annotation-beanoverriding-mockitobean[`@MockitoBean` and `@MockitoSpyBean`]

View File

@@ -0,0 +1,130 @@
[[spring-testing-annotation-beanoverriding]]
= Bean Overriding in Tests
Bean Overriding in Tests refers to the ability to override specific beans in the Context
for a test class, by annotating one or more fields in said test class.
NOTE: This is intended as a less risky alternative to the practice of registering a bean via
`@Bean` with the `DefaultListableBeanFactory` `setAllowBeanDefinitionOverriding` set to
`true`.
The Spring Testing Framework provides two sets of annotations presented below. One relies
purely on Spring, while the second set relies on the Mockito third party library.
[[spring-testing-annotation-beanoverriding-testbean]]
== `@TestBean`
`@TestBean` is used on a test class field to override a specific bean with an instance
provided by a conventionally named static method.
By default, the bean name and the associated static method name are derived from the
annotated field's name, but the annotation allows for specific values to be provided.
The `@TestBean` annotation uses the `REPLACE_DEFINITION`
xref:#spring-testing-annotation-beanoverriding-extending[strategy for test bean overriding].
The following example shows how to fully configure the `@TestBean` annotation, with
explicit values equivalent to the default:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
class OverrideBeanTests {
@TestBean(name = "service", methodName = "serviceTestOverride") // <1>
private CustomService service;
// test case body...
private static CustomService serviceTestOverride() { // <2>
return new MyFakeCustomService();
}
}
----
<1> Mark a field for bean overriding in this test class
<2> The result of this static method will be used as the instance and injected into the field
======
[[spring-testing-annotation-beanoverriding-mockitobean]]
== `@MockitoBean` and `@MockitoSpyBean`
`@MockitoBean` and `@MockitoSpyBean` are used on a test class field to override a bean
with a mocking and spying instance, respectively. In the later case, the original bean
definition is not replaced but instead an early instance is captured and wrapped by the
spy.
By default, the name of the bean to override is derived from the annotated field's name,
but both annotations allows for a specific `name` to be provided. Each annotation also
defines Mockito-specific attributes to fine-tune the mocking details.
The `@MockitoBean` annotation uses the `CREATE_OR_REPLACE_DEFINITION`
xref:#spring-testing-annotation-beanoverriding-extending[strategy for test bean overriding].
The `@MockitoSpyBean` annotation uses the `WRAP_EARLY_BEAN`
xref:#spring-testing-annotation-beanoverriding-extending[strategy] and the original instance
is wrapped in a Mockito spy.
The following example shows how to configure the bean name for both `@MockitoBean` and
`@MockitoSpyBean` annotations:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
class OverrideBeanTests {
@MockitoBean(name = "service1") // <1>
private CustomService mockService;
@MockitoSpyBean(name = "service2") // <2>
private CustomService spyService; // <3>
// test case body...
}
----
<1> Mark `mockService` as a Mockito mock override of bean `service1` in this test class
<2> Mark `spyService` as a Mockito spy override of bean `service2` in this test class
<3> Both fields will be injected with the Mockito values (the mock and the spy respectively)
======
[[spring-testing-annotation-beanoverriding-extending]]
== Extending bean override with a custom annotation
The three annotations introduced above build upon the `@BeanOverride` meta-annotation
and associated infrastructure, which allows to define custom bean overriding variants.
In order to provide an extension, three classes are needed:
- a concrete `BeanOverrideProcessor` `<P>`
- a concrete `OverrideMetadata` created by said processor
- an annotation meta-annotated with `@BeanOverride(P.class)`
The Spring TestContext Framework includes infrastructure classes that support bean
overriding: a `BeanPostProcessor`, a `TestExecutionListener` and a `ContextCustomizerFactory`.
These are automatically registered via the Spring TestContext Framework `spring.factories`
file.
The test classes are parsed looking for any field meta-annotated with `@BeanOverride`,
instantiating the relevant `BeanOverrideProcessor` in order to register an `OverrideMetadata`.
Then the `BeanOverrideBeanPostProcessor` will use that information to alter the Context,
registering and replacing bean definitions as influenced by each metadata
`BeanOverrideStrategy`:
- `REPLACE_DEFINITION`: the bean post-processor replaces the bean definition.
If it is not present in the context, an exception is thrown.
- `CREATE_OR_REPLACE_DEFINITION`: same as above but if the bean definition is not present
in the context, one is created
- `WRAP_EARLY_BEAN`: an original instance is obtained via
`SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference(Object, String)` and
provided to the processor during `OverrideMetadata` creation.
NOTE: The Bean Overriding infrastructure works best with singleton beans. It also doesn't
include any bean resolution (unlike e.g. an `@Autowired`-annotated field). As such, the
name of the bean to override MUST be somehow provided to or computed by the
`BeanOverrideProcessor`. Typically, the end user provides the name as part of the custom
annotation's attributes, or the annotated field's name.