Skip to content

Commit e1bbdf0

Browse files
committed
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.
1 parent 90867e7 commit e1bbdf0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+3516
-3
lines changed

framework-docs/modules/ROOT/nav.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@
183183
***** xref:testing/annotations/integration-spring/annotation-sqlmergemode.adoc[]
184184
***** xref:testing/annotations/integration-spring/annotation-sqlgroup.adoc[]
185185
***** xref:testing/annotations/integration-spring/annotation-disabledinaotmode.adoc[]
186+
***** xref:testing/annotations/integration-spring/annotation-beanoverriding.adoc[]
186187
**** xref:testing/annotations/integration-junit4.adoc[]
187188
**** xref:testing/annotations/integration-junit-jupiter.adoc[]
188189
**** xref:testing/annotations/integration-meta.adoc[]

framework-docs/modules/ROOT/pages/testing/annotations/integration-spring.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,6 @@ Spring's testing annotations include the following:
2828
* xref:testing/annotations/integration-spring/annotation-sqlmergemode.adoc[`@SqlMergeMode`]
2929
* xref:testing/annotations/integration-spring/annotation-sqlgroup.adoc[`@SqlGroup`]
3030
* xref:testing/annotations/integration-spring/annotation-disabledinaotmode.adoc[`@DisabledInAotMode`]
31+
* xref:testing/annotations/integration-spring/annotation-beanoverriding.adoc#spring-testing-annotation-beanoverriding-testbean[`@TestBean`]
32+
* xref:testing/annotations/integration-spring/annotation-beanoverriding.adoc#spring-testing-annotation-beanoverriding-mockitobean[`@MockitoBean` and `@MockitoSpyBean`]
3133

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
[[spring-testing-annotation-beanoverriding]]
2+
= Bean Overriding in Tests
3+
4+
Bean Overriding in Tests refers to the ability to override specific beans in the Context
5+
for a test class, by annotating one or more fields in said test class.
6+
7+
NOTE: This is intended as a less risky alternative to the practice of registering a bean via
8+
`@Bean` with the `DefaultListableBeanFactory` `setAllowBeanDefinitionOverriding` set to
9+
`true`.
10+
11+
The Spring Testing Framework provides two sets of annotations presented below. One relies
12+
purely on Spring, while the second set relies on the Mockito third party library.
13+
14+
[[spring-testing-annotation-beanoverriding-testbean]]
15+
== `@TestBean`
16+
17+
`@TestBean` is used on a test class field to override a specific bean with an instance
18+
provided by a conventionally named static method.
19+
20+
By default, the bean name and the associated static method name are derived from the
21+
annotated field's name, but the annotation allows for specific values to be provided.
22+
23+
The `@TestBean` annotation uses the `REPLACE_DEFINITION`
24+
xref:#spring-testing-annotation-beanoverriding-extending[strategy for test bean overriding].
25+
26+
The following example shows how to fully configure the `@TestBean` annotation, with
27+
explicit values equivalent to the default:
28+
29+
[tabs]
30+
======
31+
Java::
32+
+
33+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
34+
----
35+
class OverrideBeanTests {
36+
@TestBean(name = "service", methodName = "serviceTestOverride") // <1>
37+
private CustomService service;
38+
39+
// test case body...
40+
41+
private static CustomService serviceTestOverride() { // <2>
42+
return new MyFakeCustomService();
43+
}
44+
}
45+
----
46+
<1> Mark a field for bean overriding in this test class
47+
<2> The result of this static method will be used as the instance and injected into the field
48+
======
49+
50+
51+
[[spring-testing-annotation-beanoverriding-mockitobean]]
52+
== `@MockitoBean` and `@MockitoSpyBean`
53+
54+
`@MockitoBean` and `@MockitoSpyBean` are used on a test class field to override a bean
55+
with a mocking and spying instance, respectively. In the later case, the original bean
56+
definition is not replaced but instead an early instance is captured and wrapped by the
57+
spy.
58+
59+
By default, the name of the bean to override is derived from the annotated field's name,
60+
but both annotations allows for a specific `name` to be provided. Each annotation also
61+
defines Mockito-specific attributes to fine-tune the mocking details.
62+
63+
The `@MockitoBean` annotation uses the `CREATE_OR_REPLACE_DEFINITION`
64+
xref:#spring-testing-annotation-beanoverriding-extending[strategy for test bean overriding].
65+
66+
The `@MockitoSpyBean` annotation uses the `WRAP_EARLY_BEAN`
67+
xref:#spring-testing-annotation-beanoverriding-extending[strategy] and the original instance
68+
is wrapped in a Mockito spy.
69+
70+
The following example shows how to configure the bean name for both `@MockitoBean` and
71+
`@MockitoSpyBean` annotations:
72+
73+
[tabs]
74+
======
75+
Java::
76+
+
77+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
78+
----
79+
class OverrideBeanTests {
80+
@MockitoBean(name = "service1") // <1>
81+
private CustomService mockService;
82+
83+
@MockitoSpyBean(name = "service2") // <2>
84+
private CustomService spyService; // <3>
85+
86+
// test case body...
87+
}
88+
----
89+
<1> Mark `mockService` as a Mockito mock override of bean `service1` in this test class
90+
<2> Mark `spyService` as a Mockito spy override of bean `service2` in this test class
91+
<3> Both fields will be injected with the Mockito values (the mock and the spy respectively)
92+
======
93+
94+
95+
[[spring-testing-annotation-beanoverriding-extending]]
96+
== Extending bean override with a custom annotation
97+
98+
The three annotations introduced above build upon the `@BeanOverride` meta-annotation
99+
and associated infrastructure, which allows to define custom bean overriding variants.
100+
101+
In order to provide an extension, three classes are needed:
102+
- a concrete `BeanOverrideProcessor` `<P>`
103+
- a concrete `OverrideMetadata` created by said processor
104+
- an annotation meta-annotated with `@BeanOverride(P.class)`
105+
106+
The Spring TestContext Framework includes infrastructure classes that support bean
107+
overriding: a `BeanPostProcessor`, a `TestExecutionListener` and a `ContextCustomizerFactory`.
108+
These are automatically registered via the Spring TestContext Framework `spring.factories`
109+
file.
110+
111+
The test classes are parsed looking for any field meta-annotated with `@BeanOverride`,
112+
instantiating the relevant `BeanOverrideProcessor` in order to register an `OverrideMetadata`.
113+
114+
Then the `BeanOverrideBeanPostProcessor` will use that information to alter the Context,
115+
registering and replacing bean definitions as influenced by each metadata
116+
`BeanOverrideStrategy`:
117+
118+
- `REPLACE_DEFINITION`: the bean post-processor replaces the bean definition.
119+
If it is not present in the context, an exception is thrown.
120+
- `CREATE_OR_REPLACE_DEFINITION`: same as above but if the bean definition is not present
121+
in the context, one is created
122+
- `WRAP_EARLY_BEAN`: an original instance is obtained via
123+
`SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference(Object, String)` and
124+
provided to the processor during `OverrideMetadata` creation.
125+
126+
NOTE: The Bean Overriding infrastructure works best with singleton beans. It also doesn't
127+
include any bean resolution (unlike e.g. an `@Autowired`-annotated field). As such, the
128+
name of the bean to override MUST be somehow provided to or computed by the
129+
`BeanOverrideProcessor`. Typically, the end user provides the name as part of the custom
130+
annotation's attributes, or the annotated field's name.

spring-test/spring-test.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ dependencies {
4242
optional("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
4343
optional("org.junit.jupiter:junit-jupiter-api")
4444
optional("org.junit.platform:junit-platform-launcher") // for AOT processing
45+
optional("org.mockito:mockito-core")
4546
optional("org.seleniumhq.selenium:htmlunit-driver") {
4647
exclude group: "commons-logging", module: "commons-logging"
4748
exclude group: "net.bytebuddy", module: "byte-buddy"
@@ -79,6 +80,7 @@ dependencies {
7980
testImplementation("org.hibernate:hibernate-validator")
8081
testImplementation("org.hsqldb:hsqldb")
8182
testImplementation("org.junit.platform:junit-platform-testkit")
83+
testImplementation("org.mockito:mockito-core")
8284
testRuntimeOnly("com.sun.xml.bind:jaxb-core")
8385
testRuntimeOnly("com.sun.xml.bind:jaxb-impl")
8486
testRuntimeOnly("org.glassfish:jakarta.el")
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.bean.override;
18+
19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
23+
24+
/**
25+
* Mark an annotation as eligible for Bean Override parsing.
26+
* This meta-annotation provides a {@link BeanOverrideProcessor} class which
27+
* must be capable of handling the annotated annotation.
28+
*
29+
* <p>Target annotation must have a {@link RetentionPolicy} of {@code RUNTIME}
30+
* and be applicable to {@link java.lang.reflect.Field Fields} only.
31+
* @see BeanOverrideBeanPostProcessor
32+
*
33+
* @author Simon Baslé
34+
* @since 6.2
35+
*/
36+
@Retention(RetentionPolicy.RUNTIME)
37+
@Target({ElementType.ANNOTATION_TYPE})
38+
public @interface BeanOverride {
39+
40+
/**
41+
* A {@link BeanOverrideProcessor} implementation class by which the target
42+
* annotation should be processed. Implementations must have a no-argument
43+
* constructor.
44+
*/
45+
Class<? extends BeanOverrideProcessor> value();
46+
}

0 commit comments

Comments
 (0)