From 43f13e4d30db47495084235cca99ffb445ffedbf Mon Sep 17 00:00:00 2001
From: Sam Brannen <104798+sbrannen@users.noreply.github.com>
Date: Wed, 15 Jan 2025 10:49:27 +0100
Subject: [PATCH 1/2] =?UTF-8?q?Cross=20reference=20@=E2=81=A0NestedTestCon?=
=?UTF-8?q?figuration=20for=20Bean=20Overrides?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../test/context/NestedTestConfiguration.java | 5 ++++-
.../test/context/bean/override/convention/TestBean.java | 6 +++++-
.../test/context/bean/override/mockito/MockitoBean.java | 4 ++++
.../test/context/bean/override/mockito/MockitoSpyBean.java | 6 +++++-
4 files changed, 18 insertions(+), 3 deletions(-)
diff --git a/spring-test/src/main/java/org/springframework/test/context/NestedTestConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/NestedTestConfiguration.java
index d7102667e0c9..9b716cc7f4ef 100644
--- a/spring-test/src/main/java/org/springframework/test/context/NestedTestConfiguration.java
+++ b/spring-test/src/main/java/org/springframework/test/context/NestedTestConfiguration.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -82,6 +82,9 @@
*
{@link ActiveProfiles @ActiveProfiles}
* {@link TestPropertySource @TestPropertySource}
* {@link DynamicPropertySource @DynamicPropertySource}
+ * {@link org.springframework.test.context.bean.override.convention.TestBean @TestBean}
+ * {@link org.springframework.test.context.bean.override.mockito.MockitoBean @MockitoBean}
+ * {@link org.springframework.test.context.bean.override.mockito.MockitoSpyBean @MockitoSpyBean}
* {@link org.springframework.test.annotation.DirtiesContext @DirtiesContext}
* {@link org.springframework.transaction.annotation.Transactional @Transactional}
* {@link org.springframework.test.annotation.Rollback @Rollback}
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java
index f6a63ed4574f..9393a17ed0cb 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -110,6 +110,10 @@
* {@code protected}, package-private (default visibility), or {@code private}
* depending on the needs or coding practices of the project.
*
+ * {@code @TestBean} fields will be inherited from an enclosing test class by default. See
+ * {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration}
+ * for details.
+ *
* @author Simon Baslé
* @author Stephane Nicoll
* @author Sam Brannen
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java
index f01d0fc10b53..640a75bb22e9 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java
@@ -63,6 +63,10 @@
* (default visibility), or {@code private} depending on the needs or coding
* practices of the project.
*
+ *
{@code @MockitoBean} fields will be inherited from an enclosing test class by default.
+ * See {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration}
+ * for details.
+ *
* @author Simon Baslé
* @author Sam Brannen
* @since 6.2
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBean.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBean.java
index 6320a54845b1..d10674b75331 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBean.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBean.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -56,6 +56,10 @@
* (default visibility), or {@code private} depending on the needs or coding
* practices of the project.
*
+ *
{@code @MockitoSpyBean} fields will be inherited from an enclosing test class by default.
+ * See {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration}
+ * for details.
+ *
* @author Simon Baslé
* @author Sam Brannen
* @since 6.2
From 405eb7c1474d2df5c55338e6bc11be50063f332b Mon Sep 17 00:00:00 2001
From: Sam Brannen <104798+sbrannen@users.noreply.github.com>
Date: Fri, 3 Jan 2025 12:09:04 +0200
Subject: [PATCH 2/2] =?UTF-8?q?Support=20@=E2=81=A0MockitoBean=20at=20the?=
=?UTF-8?q?=20type=20level=20on=20test=20classes?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Prior to this commit, @MockitoBean could only be declared on fields
within test classes, which prevented developers from being able to
easily reuse mock configuration across a test suite.
With this commit, @MockitoBean is now supported at the type level on
test classes, their superclasses, and interfaces implemented by those
classes. @MockitoBean is also supported on enclosing classes for
@Nested test classes, their superclasses, and interfaces implemented
by those classes, while honoring @NestedTestConfiguration semantics.
In addition, @MockitoBean:
- has a new `types` attribute that can be used to declare the type or
types to mock when @MockitoBean is declared at the type level
- can be declared as a repeatable annotation at the type level
- can be declared as a meta-annotation on a custom composed annotation
which can be reused across a test suite (see the @SharedMocks
example in the reference manual)
To support these new features, this commit also includes the following
changes.
- The `field` property in BeanOverrideHandler is now @Nullable.
- BeanOverrideProcessor has a new `default` createHandlers() method
which is invoked when a @BeanOverride annotation is found at the
type level.
- MockitoBeanOverrideProcessor implements the new createHandlers()
method.
- The internal findHandlers() method in BeanOverrideHandler has been
completely overhauled.
- The @MockitoBean and @MockitoSpyBean section of the reference
manual has been completely overhauled.
Closes gh-33925
---
.../annotation-mockitobean.adoc | 142 ++++++++++++---
.../context/bean/override/BeanOverride.java | 12 +-
.../BeanOverrideBeanFactoryPostProcessor.java | 70 ++++----
.../BeanOverrideContextCustomizerFactory.java | 6 +-
.../bean/override/BeanOverrideHandler.java | 132 ++++++++++----
.../bean/override/BeanOverrideProcessor.java | 38 +++-
.../bean/override/BeanOverrideRegistry.java | 4 +-
.../AbstractMockitoBeanOverrideHandler.java | 2 +-
.../bean/override/mockito/MockitoBean.java | 75 ++++++--
.../mockito/MockitoBeanOverrideHandler.java | 8 +-
.../mockito/MockitoBeanOverrideProcessor.java | 26 +++
.../bean/override/mockito/MockitoBeans.java | 41 +++++
.../bean/override/BeanOverrideTestUtils.java | 4 +
.../MockitoBeanOverrideHandlerTests.java | 101 ++++++++++-
.../MockitoBeanOverrideProcessorTests.java | 164 +++++++++++++++---
.../MockitoBeansByNameIntegrationTests.java | 102 +++++++++++
.../MockitoBeansByTypeIntegrationTests.java | 147 ++++++++++++++++
.../mockito/mockbeans/MockitoBeansTests.java | 68 ++++++++
.../override/mockito/mockbeans/Service.java | 21 +++
.../override/mockito/mockbeans/Service01.java | 20 +++
.../override/mockito/mockbeans/Service02.java | 20 +++
.../override/mockito/mockbeans/Service03.java | 20 +++
.../override/mockito/mockbeans/Service04.java | 20 +++
.../override/mockito/mockbeans/Service05.java | 20 +++
.../override/mockito/mockbeans/Service06.java | 20 +++
.../override/mockito/mockbeans/Service07.java | 20 +++
.../override/mockito/mockbeans/Service08.java | 20 +++
.../override/mockito/mockbeans/Service09.java | 20 +++
.../override/mockito/mockbeans/Service10.java | 20 +++
.../override/mockito/mockbeans/Service11.java | 20 +++
.../override/mockito/mockbeans/Service12.java | 20 +++
.../override/mockito/mockbeans/Service13.java | 20 +++
.../mockito/mockbeans/SharedMocks.java | 31 ++++
.../mockito/mockbeans/TestInterface01.java | 23 +++
.../mockito/mockbeans/TestInterface08.java | 23 +++
.../mockito/mockbeans/TestInterface11.java | 23 +++
36 files changed, 1379 insertions(+), 144 deletions(-)
create mode 100644 spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeans.java
create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/MockitoBeansByNameIntegrationTests.java
create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/MockitoBeansByTypeIntegrationTests.java
create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/MockitoBeansTests.java
create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service.java
create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service01.java
create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service02.java
create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service03.java
create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service04.java
create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service05.java
create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service06.java
create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service07.java
create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service08.java
create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service09.java
create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service10.java
create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service11.java
create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service12.java
create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service13.java
create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/SharedMocks.java
create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/TestInterface01.java
create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/TestInterface08.java
create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/TestInterface11.java
diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc
index d46508f3d9a4..6bb17af198d4 100644
--- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc
+++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc
@@ -11,29 +11,21 @@ If multiple candidates match, `@Qualifier` can be provided to narrow the candida
override. Alternatively, a candidate whose bean name matches the name of the field will
match.
-When using `@MockitoBean`, a new bean will be created if a corresponding bean does not
-exist. However, if you would like for the test to fail when a corresponding bean does not
-exist, you can set the `enforceOverride` attribute to `true` – for example,
-`@MockitoBean(enforceOverride = true)`.
-
-To use a by-name override rather than a by-type override, specify the `name` attribute
-of the annotation.
-
[WARNING]
====
Qualifiers, including the name of the field, are used to determine if a separate
`ApplicationContext` needs to be created. If you are using this feature to mock or spy
-the same bean in several tests, make sure to name the field consistently to avoid
+the same bean in several test classes, make sure to name the field consistently to avoid
creating unnecessary contexts.
====
-Each annotation also defines Mockito-specific attributes to fine-tune the mocking details.
+Each annotation also defines Mockito-specific attributes to fine-tune the mocking behavior.
-By default, the `@MockitoBean` annotation uses the `REPLACE_OR_CREATE`
+The `@MockitoBean` annotation uses the `REPLACE_OR_CREATE`
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy for test bean overriding].
-If no existing bean matches, a new bean is created on the fly. As mentioned previously,
-you can switch to the `REPLACE` strategy by setting the `enforceOverride` attribute to
-`true`.
+If no existing bean matches, a new bean is created on the fly. However, you can switch to
+the `REPLACE` strategy by setting the `enforceOverride` attribute to `true`. See the
+following section for an example.
The `@MockitoSpyBean` annotation uses the `WRAP`
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy],
@@ -61,6 +53,17 @@ Such fields can therefore be `public`, `protected`, package-private (default vis
or `private` depending on the needs or coding practices of the project.
====
+[[spring-testing-annotation-beanoverriding-mockitobean-examples]]
+== `@MockitoBean` Examples
+
+When using `@MockitoBean`, a new bean will be created if a corresponding bean does not
+exist. However, if you would like for the test to fail when a corresponding bean does not
+exist, you can set the `enforceOverride` attribute to `true` – for example,
+`@MockitoBean(enforceOverride = true)`.
+
+To use a by-name override rather than a by-type override, specify the `name` (or `value`)
+attribute of the annotation.
+
The following example shows how to use the default behavior of the `@MockitoBean` annotation:
[tabs]
@@ -69,11 +72,13 @@ Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
- class OverrideBeanTests {
+ @SpringJUnitConfig(TestConfig.class)
+ class BeanOverrideTests {
+
@MockitoBean // <1>
CustomService customService;
- // test case body...
+ // tests...
}
----
<1> Replace the bean with type `CustomService` with a Mockito `mock`.
@@ -82,8 +87,8 @@ Java::
In the example above, we are creating a mock for `CustomService`. If more than one bean
of that type exists, the bean named `customService` is considered. Otherwise, the test
will fail, and you will need to provide a qualifier of some sort to identify which of the
-`CustomService` beans you want to override. If no such bean exists, a bean definition
-will be created with an auto-generated bean name.
+`CustomService` beans you want to override. If no such bean exists, a bean will be
+created with an auto-generated bean name.
The following example uses a by-name lookup, rather than a by-type lookup:
@@ -93,20 +98,43 @@ Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
- class OverrideBeanTests {
+ @SpringJUnitConfig(TestConfig.class)
+ class BeanOverrideTests {
+
@MockitoBean("service") // <1>
CustomService customService;
- // test case body...
+ // tests...
}
----
<1> Replace the bean named `service` with a Mockito `mock`.
======
-If no bean definition named `service` exists, one is created.
+If no bean named `service` exists, one is created.
+
+`@MockitoBean` can also be used at the type level:
+
+- on a test class or any superclass or implemented interface in the type hierarchy above
+ the test class
+- on an enclosing class for a `@Nested` test class or on any class or interface in the
+ type hierarchy or enclosing class hierarchy above the `@Nested` test class
-The following example shows how to use the default behavior of the `@MockitoSpyBean` annotation:
+When `@MockitoBean` is declared at the type level, the type of bean (or beans) to mock
+must be supplied via the `types` attribute – for example,
+`@MockitoBean(types = {OrderService.class, UserService.class})`. If multiple candidates
+exist in the application context, you can explicitly specify a bean name to mock by
+setting the `name` attribute. Note, however, that the `types` attribute must contain a
+single type if an explicit bean `name` is configured – for example,
+`@MockitoBean(name = "ps1", types = PrintingService.class)`.
+
+To support reuse of mock configuration, `@MockitoBean` may be used as a meta-annotation
+to create custom _composed annotations_ — for example, to define common mock
+configuration in a single annotation that can be reused across a test suite.
+`@MockitoBean` can also be used as a repeatable annotation at the type level — for
+example, to mock several beans by name.
+
+The following `@SharedMocks` annotation registers two mocks by-type and one mock by-name.
[tabs]
======
@@ -114,11 +142,70 @@ Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
- class OverrideBeanTests {
+ @Target(ElementType.TYPE)
+ @Retention(RetentionPolicy.RUNTIME)
+ @MockitoBean(types = {OrderService.class, UserService.class}) // <1>
+ @MockitoBean(name = "ps1", types = PrintingService.class) // <2>
+ public @interface SharedMocks {
+ }
+----
+<1> Register `OrderService` and `UserService` mocks by-type.
+<2> Register `PrintingService` mock by-name.
+======
+
+The following demonstrates how `@SharedMocks` can be used on a test class.
+
+[tabs]
+======
+Java::
++
+[source,java,indent=0,subs="verbatim,quotes"]
+----
+ @SpringJUnitConfig(TestConfig.class)
+ @SharedMocks // <1>
+ class BeanOverrideTests {
+
+ @Autowired OrderService orderService; // <2>
+
+ @Autowired UserService userService; // <2>
+
+ @Autowired PrintingService ps1; // <2>
+
+ // Inject other components that rely on the mocks.
+
+ @Test
+ void testThatDependsOnMocks() {
+ // ...
+ }
+ }
+----
+<1> Register common mocks via the custom `@SharedMocks` annotation.
+<2> Optionally inject mocks to _stub_ or _verify_ them.
+======
+
+TIP: The mocks can also be injected into `@Configuration` classes or other test-related
+components in the `ApplicationContext` in order to configure them with Mockito's stubbing
+APIs.
+
+[[spring-testing-annotation-beanoverriding-mockitospybean-examples]]
+== `@MockitoSpyBean` Examples
+
+The following example shows how to use the default behavior of the `@MockitoSpyBean`
+annotation:
+
+[tabs]
+======
+Java::
++
+[source,java,indent=0,subs="verbatim,quotes"]
+----
+ @SpringJUnitConfig(TestConfig.class)
+ class BeanOverrideTests {
+
@MockitoSpyBean // <1>
CustomService customService;
- // test case body...
+ // tests...
}
----
<1> Wrap the bean with type `CustomService` with a Mockito `spy`.
@@ -137,12 +224,13 @@ Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
- class OverrideBeanTests {
+ @SpringJUnitConfig(TestConfig.class)
+ class BeanOverrideTests {
+
@MockitoSpyBean("service") // <1>
CustomService customService;
- // test case body...
-
+ // tests...
}
----
<1> Wrap the bean named `service` with a Mockito `spy`.
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverride.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverride.java
index 910eb5019562..9efd7948c547 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverride.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverride.java
@@ -25,15 +25,19 @@
import org.springframework.aot.hint.annotation.Reflective;
/**
- * Mark a composed annotation as eligible for Bean Override processing.
+ * Mark a composed annotation as eligible for Bean Override processing.
*
*
Specifying this annotation registers the configured {@link BeanOverrideProcessor}
* which must be capable of handling the composed annotation and its attributes.
*
- *
Since the composed annotation should only be applied to non-static fields, it is
- * expected that it is meta-annotated with {@link Target @Target(ElementType.FIELD)}.
+ *
Since the composed annotation will typically only be applied to non-static
+ * fields, it is expected that the composed annotation is meta-annotated with
+ * {@link Target @Target(ElementType.FIELD)}. However, certain bean override
+ * annotations may be declared with an additional {@code ElementType.TYPE} target
+ * for use at the type level, as is the case for {@code @MockitoBean} which can
+ * be declared on a field, test class, or test interface.
*
- *
For concrete examples, see
+ *
For concrete examples of such composed annotations, see
* {@link org.springframework.test.context.bean.override.convention.TestBean @TestBean},
* {@link org.springframework.test.context.bean.override.mockito.MockitoBean @MockitoBean}, and
* {@link org.springframework.test.context.bean.override.mockito.MockitoSpyBean @MockitoSpyBean}.
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java
index 4d1f0f0d8532..c9224bc70b3a 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -104,11 +104,10 @@ private void registerBeanOverride(ConfigurableListableBeanFactory beanFactory, B
Set generatedBeanNames) {
String beanName = handler.getBeanName();
- Field field = handler.getField();
- Assert.state(!BeanFactoryUtils.isFactoryDereference(beanName),() -> """
- Unable to override bean '%s' for field '%s.%s': a FactoryBean cannot be overridden. \
- To override the bean created by the FactoryBean, remove the '&' prefix.""".formatted(
- beanName, field.getDeclaringClass().getSimpleName(), field.getName()));
+ Assert.state(!BeanFactoryUtils.isFactoryDereference(beanName), () -> """
+ Unable to override bean '%s'%s: a FactoryBean cannot be overridden. \
+ To override the bean created by the FactoryBean, remove the '&' prefix."""
+ .formatted(beanName, forField(handler.getField())));
switch (handler.getStrategy()) {
case REPLACE -> replaceOrCreateBean(beanFactory, handler, generatedBeanNames, true);
@@ -134,7 +133,6 @@ private void replaceOrCreateBean(ConfigurableListableBeanFactory beanFactory, Be
// 4) Create bean by-name, with a provided name
String beanName = handler.getBeanName();
- Field field = handler.getField();
BeanDefinition existingBeanDefinition = null;
if (beanName == null) {
beanName = getBeanNameForType(beanFactory, handler, requireExistingBean);
@@ -169,11 +167,10 @@ private void replaceOrCreateBean(ConfigurableListableBeanFactory beanFactory, Be
existingBeanDefinition = beanFactory.getBeanDefinition(beanName);
}
else if (requireExistingBean) {
- throw new IllegalStateException("""
- Unable to replace bean: there is no bean with name '%s' and type %s \
- (as required by field '%s.%s')."""
- .formatted(beanName, handler.getBeanType(),
- field.getDeclaringClass().getSimpleName(), field.getName()));
+ Field field = handler.getField();
+ throw new IllegalStateException(
+ "Unable to replace bean: there is no bean with name '%s' and type %s%s."
+ .formatted(beanName, handler.getBeanType(), requiredByField(field)));
}
// 4) We are creating a bean by-name with the provided beanName.
}
@@ -264,13 +261,11 @@ private void wrapBean(ConfigurableListableBeanFactory beanFactory, BeanOverrideH
else {
String message = "Unable to select a bean to wrap: ";
if (candidateCount == 0) {
- message += "there are no beans of type %s (as required by field '%s.%s')."
- .formatted(beanType, field.getDeclaringClass().getSimpleName(), field.getName());
+ message += "there are no beans of type %s%s.".formatted(beanType, requiredByField(field));
}
else {
- message += "found %d beans of type %s (as required by field '%s.%s'): %s"
- .formatted(candidateCount, beanType, field.getDeclaringClass().getSimpleName(),
- field.getName(), candidateNames);
+ message += "found %d beans of type %s%s: %s"
+ .formatted(candidateCount, beanType, requiredByField(field), candidateNames);
}
throw new IllegalStateException(message);
}
@@ -281,11 +276,9 @@ private void wrapBean(ConfigurableListableBeanFactory beanFactory, BeanOverrideH
// We are wrapping an existing bean by-name.
Set candidates = getExistingBeanNamesByType(beanFactory, handler, false);
if (!candidates.contains(beanName)) {
- throw new IllegalStateException("""
- Unable to wrap bean: there is no bean with name '%s' and type %s \
- (as required by field '%s.%s')."""
- .formatted(beanName, beanType, field.getDeclaringClass().getSimpleName(),
- field.getName()));
+ throw new IllegalStateException(
+ "Unable to wrap bean: there is no bean with name '%s' and type %s%s."
+ .formatted(beanName, beanType, requiredByField(field)));
}
}
@@ -308,8 +301,8 @@ private String getBeanNameForType(ConfigurableListableBeanFactory beanFactory, B
else if (candidateCount == 0) {
if (requireExistingBean) {
throw new IllegalStateException(
- "Unable to override bean: there are no beans of type %s (as required by field '%s.%s')."
- .formatted(beanType, field.getDeclaringClass().getSimpleName(), field.getName()));
+ "Unable to override bean: there are no beans of type %s%s."
+ .formatted(beanType, requiredByField(field)));
}
return null;
}
@@ -320,14 +313,14 @@ else if (candidateCount == 0) {
}
throw new IllegalStateException(
- "Unable to select a bean to override: found %d beans of type %s (as required by field '%s.%s'): %s"
- .formatted(candidateCount, beanType, field.getDeclaringClass().getSimpleName(),
- field.getName(), candidateNames));
+ "Unable to select a bean to override: found %d beans of type %s%s: %s"
+ .formatted(candidateCount, beanType, requiredByField(field), candidateNames));
}
private Set getExistingBeanNamesByType(ConfigurableListableBeanFactory beanFactory, BeanOverrideHandler handler,
boolean checkAutowiredCandidate) {
+ Field field = handler.getField();
ResolvableType resolvableType = handler.getBeanType();
Class> type = resolvableType.toClass();
@@ -345,16 +338,16 @@ private Set getExistingBeanNamesByType(ConfigurableListableBeanFactory b
}
// Filter out non-matching autowire candidates.
- if (checkAutowiredCandidate) {
- DependencyDescriptor descriptor = new DependencyDescriptor(handler.getField(), true);
+ if (field != null && checkAutowiredCandidate) {
+ DependencyDescriptor descriptor = new DependencyDescriptor(field, true);
beanNames.removeIf(beanName -> !beanFactory.isAutowireCandidate(beanName, descriptor));
}
// Filter out scoped proxy targets.
beanNames.removeIf(ScopedProxyUtils::isScopedTarget);
// In case of multiple matches, fall back on the field's name as a last resort.
- if (beanNames.size() > 1) {
- String fieldName = handler.getField().getName();
+ if (field != null && beanNames.size() > 1) {
+ String fieldName = field.getName();
if (beanNames.contains(fieldName)) {
return Set.of(fieldName);
}
@@ -452,4 +445,19 @@ private static void destroySingleton(ConfigurableListableBeanFactory beanFactory
dlbf.destroySingleton(beanName);
}
+ private static String forField(@Nullable Field field) {
+ if (field == null) {
+ return "";
+ }
+ return " for field '%s.%s'".formatted(field.getDeclaringClass().getSimpleName(), field.getName());
+ }
+
+ private static String requiredByField(@Nullable Field field) {
+ if (field == null) {
+ return "";
+ }
+ return " (as required by field '%s.%s')".formatted(
+ field.getDeclaringClass().getSimpleName(), field.getName());
+ }
+
}
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java
index 515127174a0a..dfa9c9589eef 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java
@@ -23,7 +23,6 @@
import org.springframework.lang.Nullable;
import org.springframework.test.context.ContextConfigurationAttributes;
import org.springframework.test.context.ContextCustomizerFactory;
-import org.springframework.test.context.TestContextAnnotationUtils;
import org.springframework.util.Assert;
/**
@@ -52,10 +51,7 @@ public BeanOverrideContextCustomizer createContextCustomizer(Class> testClass,
}
private void findBeanOverrideHandlers(Class> testClass, Set handlers) {
- if (TestContextAnnotationUtils.searchEnclosingClass(testClass)) {
- findBeanOverrideHandlers(testClass.getEnclosingClass(), handlers);
- }
- BeanOverrideHandler.forTestClass(testClass).forEach(handler ->
+ BeanOverrideHandler.findAllHandlers(testClass).forEach(handler ->
Assert.state(handlers.add(handler), () ->
"Duplicate BeanOverrideHandler discovered in test class %s: %s"
.formatted(testClass.getName(), handler)));
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java
index 2545fabdcc7d..db89dd6e185b 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java
@@ -17,15 +17,18 @@
package org.springframework.test.context.bean.override;
import java.lang.annotation.Annotation;
+import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
+import java.util.ArrayList;
import java.util.Collections;
+import java.util.Comparator;
import java.util.HashSet;
-import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.BiConsumer;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.config.BeanDefinition;
@@ -35,6 +38,7 @@
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.style.ToStringCreator;
import org.springframework.lang.Nullable;
+import org.springframework.test.context.TestContextAnnotationUtils;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
@@ -56,8 +60,8 @@
*
* Concrete implementations of {@code BeanOverrideHandler} can store additional
* metadata to use during override {@linkplain #createOverrideInstance instance
- * creation} — for example, based on further processing of the annotation
- * or the annotated field.
+ * creation} — for example, based on further processing of the annotation,
+ * the annotated field, or the annotated class.
*
*
NOTE: Only singleton beans can be overridden.
* Any attempt to override a non-singleton bean will result in an exception.
@@ -69,6 +73,11 @@
*/
public abstract class BeanOverrideHandler {
+ private static final Comparator> reversedMetaDistance =
+ Comparator.> comparingInt(MergedAnnotation::getDistance).reversed();
+
+
+ @Nullable
private final Field field;
private final Set qualifierAnnotations;
@@ -81,7 +90,7 @@ public abstract class BeanOverrideHandler {
private final BeanOverrideStrategy strategy;
- protected BeanOverrideHandler(Field field, ResolvableType beanType, @Nullable String beanName,
+ protected BeanOverrideHandler(@Nullable Field field, ResolvableType beanType, @Nullable String beanName,
BeanOverrideStrategy strategy) {
this.field = field;
@@ -95,57 +104,116 @@ protected BeanOverrideHandler(Field field, ResolvableType beanType, @Nullable St
* Process the given {@code testClass} and build the corresponding
* {@code BeanOverrideHandler} list derived from {@link BeanOverride @BeanOverride}
* fields in the test class and its type hierarchy.
- * This method does not search the enclosing class hierarchy.
+ *
This method does not search the enclosing class hierarchy and does not
+ * search for {@code @BeanOverride} declarations on classes or interfaces.
* @param testClass the test class to process
* @return a list of bean override handlers
- * @see org.springframework.test.context.TestContextAnnotationUtils#searchEnclosingClass(Class)
+ * @see #findAllHandlers(Class)
*/
public static List forTestClass(Class> testClass) {
- List handlers = new LinkedList<>();
- findHandlers(testClass, testClass, handlers);
+ return findHandlers(testClass, true);
+ }
+
+ /**
+ * Process the given {@code testClass} and build the corresponding
+ * {@code BeanOverrideHandler} list derived from {@link BeanOverride @BeanOverride}
+ * fields in the test class and in its type hierarchy as well as from
+ * {@code @BeanOverride} declarations on classes and interfaces.
+ * This method additionally searches for {@code @BeanOverride} declarations
+ * in the enclosing class hierarchy based on
+ * {@link TestContextAnnotationUtils#searchEnclosingClass(Class)} semantics.
+ * @param testClass the test class to process
+ * @return a list of bean override handlers
+ * @since 6.2.2
+ */
+ static List findAllHandlers(Class> testClass) {
+ return findHandlers(testClass, false);
+ }
+
+ private static List findHandlers(Class> testClass, boolean localFieldsOnly) {
+ List handlers = new ArrayList<>();
+ findHandlers(testClass, testClass, handlers, localFieldsOnly);
return handlers;
}
/**
- * Find handlers using tail recursion to ensure that "locally declared"
- * bean overrides take precedence over inherited bean overrides.
+ * Find handlers using tail recursion to ensure that "locally declared" bean overrides
+ * take precedence over inherited bean overrides.
+ * Note: the search algorithm is effectively the inverse of the algorithm used in
+ * {@link org.springframework.test.context.TestContextAnnotationUtils#findAnnotationDescriptor(Class, Class)},
+ * but with tail recursion the semantics should be the same.
+ * @param clazz the class in/on which to search
+ * @param testClass the original test class
+ * @param handlers the list of handlers found
+ * @param localFieldsOnly whether to search only on local fields within the type hierarchy
* @since 6.2.2
*/
- private static void findHandlers(Class> clazz, Class> testClass, List handlers) {
- if (clazz == null || clazz == Object.class) {
- return;
+ private static void findHandlers(Class> clazz, Class> testClass, List handlers,
+ boolean localFieldsOnly) {
+
+ // 1) Search enclosing class hierarchy.
+ if (!localFieldsOnly && TestContextAnnotationUtils.searchEnclosingClass(clazz)) {
+ findHandlers(clazz.getEnclosingClass(), testClass, handlers, localFieldsOnly);
}
- // 1) Search type hierarchy.
- findHandlers(clazz.getSuperclass(), testClass, handlers);
+ // 2) Search class hierarchy.
+ Class> superclass = clazz.getSuperclass();
+ if (superclass != null && superclass != Object.class) {
+ findHandlers(superclass, testClass, handlers, localFieldsOnly);
+ }
+
+ if (!localFieldsOnly) {
+ // 3) Search interfaces.
+ for (Class> ifc : clazz.getInterfaces()) {
+ findHandlers(ifc, testClass, handlers, localFieldsOnly);
+ }
+
+ // 4) Process current class.
+ processClass(clazz, testClass, handlers);
+ }
- // 2) Process fields in current class.
+ // 5) Process fields in current class.
ReflectionUtils.doWithLocalFields(clazz, field -> processField(field, testClass, handlers));
}
+ private static void processClass(Class> clazz, Class> testClass, List handlers) {
+ processElement(clazz, testClass, (processor, composedAnnotation) ->
+ processor.createHandlers(composedAnnotation, testClass).forEach(handlers::add));
+ }
+
private static void processField(Field field, Class> testClass, List handlers) {
AtomicBoolean overrideAnnotationFound = new AtomicBoolean();
- MergedAnnotations.from(field, DIRECT).stream(BeanOverride.class).forEach(mergedAnnotation -> {
+ processElement(field, testClass, (processor, composedAnnotation) -> {
Assert.state(!Modifier.isStatic(field.getModifiers()),
() -> "@BeanOverride field must not be static: " + field);
- MergedAnnotation> metaSource = mergedAnnotation.getMetaSource();
- Assert.state(metaSource != null, "@BeanOverride annotation must be meta-present");
-
- BeanOverride beanOverride = mergedAnnotation.synthesize();
- BeanOverrideProcessor processor = BeanUtils.instantiateClass(beanOverride.value());
- Annotation composedAnnotation = metaSource.synthesize();
-
Assert.state(overrideAnnotationFound.compareAndSet(false, true),
() -> "Multiple @BeanOverride annotations found on field: " + field);
- BeanOverrideHandler handler = processor.createHandler(composedAnnotation, testClass, field);
- handlers.add(handler);
+ handlers.add(processor.createHandler(composedAnnotation, testClass, field));
});
}
+ private static void processElement(AnnotatedElement element, Class> testClass,
+ BiConsumer consumer) {
+
+ MergedAnnotations.from(element, DIRECT)
+ .stream(BeanOverride.class)
+ .sorted(reversedMetaDistance)
+ .forEach(mergedAnnotation -> {
+ MergedAnnotation> metaSource = mergedAnnotation.getMetaSource();
+ Assert.state(metaSource != null, "@BeanOverride annotation must be meta-present");
+
+ BeanOverride beanOverride = mergedAnnotation.synthesize();
+ BeanOverrideProcessor processor = BeanUtils.instantiateClass(beanOverride.value());
+ Annotation composedAnnotation = metaSource.synthesize();
+ consumer.accept(processor, composedAnnotation);
+ });
+ }
+
/**
* Get the annotated {@link Field}.
*/
+ @Nullable
public final Field getField() {
return this.field;
}
@@ -249,7 +317,10 @@ public boolean equals(Object other) {
}
// by-type lookup
- return (Objects.equals(this.field.getName(), that.field.getName()) &&
+ if (this.field == null) {
+ return (that.field == null);
+ }
+ return (that.field != null && this.field.getName().equals(that.field.getName()) &&
this.qualifierAnnotations.equals(that.qualifierAnnotations));
}
@@ -257,7 +328,7 @@ public boolean equals(Object other) {
public int hashCode() {
int hash = Objects.hash(getClass(), this.beanType.getType(), this.beanName, this.strategy);
return (this.beanName != null ? hash : hash +
- Objects.hash(this.field.getName(), this.qualifierAnnotations));
+ Objects.hash((this.field != null ? this.field.getName() : null), this.qualifierAnnotations));
}
@Override
@@ -271,7 +342,10 @@ public String toString() {
}
- private static Set getQualifierAnnotations(Field field) {
+ private static Set getQualifierAnnotations(@Nullable Field field) {
+ if (field == null) {
+ return Collections.emptySet();
+ }
Annotation[] candidates = field.getDeclaredAnnotations();
if (candidates.length == 0) {
return Collections.emptySet();
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideProcessor.java
index 87a363adb1eb..2e1f69ec90f7 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideProcessor.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideProcessor.java
@@ -18,10 +18,13 @@
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
+import java.util.Collections;
+import java.util.List;
/**
- * Strategy interface for Bean Override processing, which creates a
- * {@link BeanOverrideHandler} that drives how the target bean is overridden.
+ * Strategy interface for Bean Override processing, which creates
+ * {@link BeanOverrideHandler} instances that drive how target beans are
+ * overridden.
*
* At least one composed annotation that is meta-annotated with
* {@link BeanOverride @BeanOverride} must be a companion of this processor and
@@ -40,12 +43,41 @@ public interface BeanOverrideProcessor {
/**
* Create a {@link BeanOverrideHandler} for the given annotated field.
+ *
This method will only be invoked when a {@link BeanOverride @BeanOverride}
+ * annotation is declared on a field — for example, if the supplied field
+ * is annotated with {@code @MockitoBean}.
* @param overrideAnnotation the composed annotation that declares the
- * {@link BeanOverride @BeanOverride} annotation which registers this processor
+ * {@code @BeanOverride} annotation which registers this processor
* @param testClass the test class to process
* @param field the annotated field
* @return the {@code BeanOverrideHandler} that should handle the given field
+ * @see #createHandlers(Annotation, Class)
*/
BeanOverrideHandler createHandler(Annotation overrideAnnotation, Class> testClass, Field field);
+ /**
+ * Create a list of {@link BeanOverrideHandler} instances for the given override
+ * annotation and test class.
+ *
This method will only be invoked when a {@link BeanOverride @BeanOverride}
+ * annotation is declared at the type level — for example, if the supplied
+ * test class is annotated with {@code @MockitoBean}.
+ *
Note that the test class may not be directly annotated with the override
+ * annotation. For example, the override annotation may have been declared
+ * on an interface, superclass, or enclosing class within the test class
+ * hierarchy.
+ *
The default implementation returns an empty list, signaling that this
+ * {@code BeanOverrideProcessor} does not support type-level {@code @BeanOverride}
+ * declarations. Can be overridden by concrete implementations to support
+ * type-level use cases.
+ * @param overrideAnnotation the composed annotation that declares the
+ * {@code @BeanOverride} annotation which registers this processor
+ * @param testClass the test class to process
+ * @return the list of {@code BeanOverrideHandlers} for the annotated class
+ * @since 6.2.2
+ * @see #createHandler(Annotation, Class, Field)
+ */
+ default List createHandlers(Annotation overrideAnnotation, Class> testClass) {
+ return Collections.emptyList();
+ }
+
}
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistry.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistry.java
index b89c4e7e5318..3afc7c885af1 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistry.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistry.java
@@ -108,9 +108,11 @@ Object wrapBeanIfNecessary(Object bean, String beanName) {
}
void inject(Object target, BeanOverrideHandler handler) {
+ Field field = handler.getField();
+ Assert.notNull(field, () -> "BeanOverrideHandler must have a non-null field: " + handler);
String beanName = this.handlerToBeanNameMap.get(handler);
Assert.state(StringUtils.hasLength(beanName), () -> "No bean found for BeanOverrideHandler: " + handler);
- inject(handler.getField(), target, beanName);
+ inject(field, target, beanName);
}
private void inject(Field field, Object target, String beanName) {
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/AbstractMockitoBeanOverrideHandler.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/AbstractMockitoBeanOverrideHandler.java
index 367c43a59c36..3c9454ce9904 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/AbstractMockitoBeanOverrideHandler.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/AbstractMockitoBeanOverrideHandler.java
@@ -38,7 +38,7 @@ abstract class AbstractMockitoBeanOverrideHandler extends BeanOverrideHandler {
private final MockReset reset;
- protected AbstractMockitoBeanOverrideHandler(Field field, ResolvableType beanType,
+ protected AbstractMockitoBeanOverrideHandler(@Nullable Field field, ResolvableType beanType,
@Nullable String beanName, BeanOverrideStrategy strategy, @Nullable MockReset reset) {
super(field, beanType, beanName, strategy);
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java
index 640a75bb22e9..7d11bd813c22 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
+import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@@ -29,17 +30,37 @@
import org.springframework.test.context.bean.override.BeanOverride;
/**
- * {@code @MockitoBean} is an annotation that can be applied to a non-static field
- * in a test class to override a bean in the test's
+ * {@code @MockitoBean} is an annotation that can be used in test classes to
+ * override beans in a test's
* {@link org.springframework.context.ApplicationContext ApplicationContext}
- * using a Mockito mock.
+ * using Mockito mocks.
*
- * By default, the bean to mock is inferred from the type of the annotated
- * field. If multiple candidates exist, a {@code @Qualifier} annotation can be
- * used to help disambiguate. In the absence of a {@code @Qualifier} annotation,
- * the name of the annotated field will be used as a fallback qualifier.
- * Alternatively, you can explicitly specify a bean name to mock by setting the
- * {@link #value() value} or {@link #name() name} attribute.
+ *
{@code @MockitoBean} can be applied in the following ways.
+ *
+ * - On a non-static field in a test class or any of its superclasses.
+ * - On a non-static field in an enclosing class for a {@code @Nested} test class
+ * or in any class in the type hierarchy or enclosing class hierarchy above the
+ * {@code @Nested} test class.
+ * - At the type level on a test class or any superclass or implemented interface
+ * in the type hierarchy above the test class.
+ * - At the type level on an enclosing class for a {@code @Nested} test class
+ * or on any class or interface in the type hierarchy or enclosing class hierarchy
+ * above the {@code @Nested} test class.
+ *
+ *
+ * When {@code @MockitoBean} is declared on a field, the bean to mock is inferred
+ * from the type of the annotated field. If multiple candidates exist, a
+ * {@code @Qualifier} annotation can be declared on the field to help disambiguate.
+ * In the absence of a {@code @Qualifier} annotation, the name of the annotated
+ * field will be used as a fallback qualifier. Alternatively, you can explicitly
+ * specify a bean name to mock by setting the {@link #value() value} or
+ * {@link #name() name} attribute.
+ *
+ *
When {@code @MockitoBean} is declared at the type level, the type of bean
+ * to mock must be supplied via the {@link #types() types} attribute. If multiple
+ * candidates exist, you can explicitly specify a bean name to mock by setting the
+ * {@link #name() name} attribute. Note, however, that the {@code types} attribute
+ * must contain a single type if an explicit bean {@code name} is configured.
*
*
A bean will be created if a corresponding bean does not exist. However, if
* you would like for the test to fail when a corresponding bean does not exist,
@@ -63,19 +84,29 @@
* (default visibility), or {@code private} depending on the needs or coding
* practices of the project.
*
- *
{@code @MockitoBean} fields will be inherited from an enclosing test class by default.
- * See {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration}
+ *
{@code @MockitoBean} fields and type-level {@code @MockitoBean} declarations
+ * will be inherited from an enclosing test class by default. See
+ * {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration}
* for details.
*
+ *
{@code @MockitoBean} may be used as a meta-annotation to create custom
+ * composed annotations — for example, to define common mock
+ * configuration in a single annotation that can be reused across a test suite.
+ * {@code @MockitoBean} can also be used as a {@linkplain Repeatable repeatable}
+ * annotation at the type level — for example, to mock several beans by
+ * {@link #name() name}.
+ *
* @author Simon Baslé
* @author Sam Brannen
* @since 6.2
+ * @see org.springframework.test.context.bean.override.mockito.MockitoBeans @MockitoBeans
* @see org.springframework.test.context.bean.override.mockito.MockitoSpyBean @MockitoSpyBean
* @see org.springframework.test.context.bean.override.convention.TestBean @TestBean
*/
-@Target(ElementType.FIELD)
+@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
+@Repeatable(MockitoBeans.class)
@BeanOverride(MockitoBeanOverrideProcessor.class)
public @interface MockitoBean {
@@ -91,13 +122,27 @@
/**
* Name of the bean to mock.
*
If left unspecified, the bean to mock is selected according to the
- * annotated field's type, taking qualifiers into account if necessary. See
- * the {@linkplain MockitoBean class-level documentation} for details.
+ * configured {@link #types() types} or the annotated field's type, taking
+ * qualifiers into account if necessary. See the {@linkplain MockitoBean
+ * class-level documentation} for details.
* @see #value()
*/
@AliasFor("value")
String name() default "";
+ /**
+ * One or more types to mock.
+ *
Defaults to none.
+ *
Each type specified will result in a mock being created and registered
+ * with the {@code ApplicationContext}.
+ *
Types must be omitted when the annotation is used on a field.
+ *
When {@code @MockitoBean} also defines a {@link #name}, this attribute
+ * can only contain a single value.
+ * @return the types to mock
+ * @since 6.2.2
+ */
+ Class>[] types() default {};
+
/**
* Extra interfaces that should also be declared by the mock.
*
Defaults to none.
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandler.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandler.java
index 190999fb5dad..32a2f4e08096 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandler.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandler.java
@@ -57,13 +57,17 @@ class MockitoBeanOverrideHandler extends AbstractMockitoBeanOverrideHandler {
private final boolean serializable;
- MockitoBeanOverrideHandler(Field field, ResolvableType typeToMock, MockitoBean mockitoBean) {
+ MockitoBeanOverrideHandler(ResolvableType typeToMock, MockitoBean mockitoBean) {
+ this(null, typeToMock, mockitoBean);
+ }
+
+ MockitoBeanOverrideHandler(@Nullable Field field, ResolvableType typeToMock, MockitoBean mockitoBean) {
this(field, typeToMock, (!mockitoBean.name().isBlank() ? mockitoBean.name() : null),
(mockitoBean.enforceOverride() ? REPLACE : REPLACE_OR_CREATE),
mockitoBean.reset(), mockitoBean.extraInterfaces(), mockitoBean.answers(), mockitoBean.serializable());
}
- private MockitoBeanOverrideHandler(Field field, ResolvableType typeToMock, @Nullable String beanName,
+ private MockitoBeanOverrideHandler(@Nullable Field field, ResolvableType typeToMock, @Nullable String beanName,
BeanOverrideStrategy strategy, MockReset reset, Class>[] extraInterfaces, Answers answers,
boolean serializable) {
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessor.java
index cc67a9488e64..62c07d594153 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessor.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessor.java
@@ -18,15 +18,20 @@
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
import org.springframework.core.ResolvableType;
+import org.springframework.test.context.bean.override.BeanOverrideHandler;
import org.springframework.test.context.bean.override.BeanOverrideProcessor;
+import org.springframework.util.Assert;
/**
* {@link BeanOverrideProcessor} implementation that provides support for
* {@link MockitoBean @MockitoBean} and {@link MockitoSpyBean @MockitoSpyBean}.
*
* @author Simon Baslé
+ * @author Sam Brannen
* @since 6.2
* @see MockitoBean @MockitoBean
* @see MockitoSpyBean @MockitoSpyBean
@@ -36,6 +41,8 @@ class MockitoBeanOverrideProcessor implements BeanOverrideProcessor {
@Override
public AbstractMockitoBeanOverrideHandler createHandler(Annotation overrideAnnotation, Class> testClass, Field field) {
if (overrideAnnotation instanceof MockitoBean mockitoBean) {
+ Assert.state(mockitoBean.types().length == 0,
+ "The @MockitoBean 'types' attribute must be omitted when declared on a field");
return new MockitoBeanOverrideHandler(field, ResolvableType.forField(field, testClass), mockitoBean);
}
else if (overrideAnnotation instanceof MockitoSpyBean spyBean) {
@@ -47,4 +54,23 @@ else if (overrideAnnotation instanceof MockitoSpyBean spyBean) {
.formatted(field.getDeclaringClass().getName(), field.getName()));
}
+ @Override
+ public List createHandlers(Annotation overrideAnnotation, Class> testClass) {
+ if (!(overrideAnnotation instanceof MockitoBean mockitoBean)) {
+ throw new IllegalStateException("""
+ Invalid annotation passed to MockitoBeanOverrideProcessor: \
+ expected @MockitoBean on test class """ + testClass.getName());
+ }
+ Class>[] types = mockitoBean.types();
+ Assert.state(types.length > 0,
+ "The @MockitoBean 'types' attribute must not be empty when declared on a class");
+ Assert.state(mockitoBean.name().isEmpty() || types.length == 1,
+ "The @MockitoBean 'name' attribute cannot be used when mocking multiple types");
+ List handlers = new ArrayList<>();
+ for (Class> type : types) {
+ handlers.add(new MockitoBeanOverrideHandler(ResolvableType.forClass(type), mockitoBean));
+ }
+ return handlers;
+ }
+
}
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeans.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeans.java
new file mode 100644
index 000000000000..3bf80b47d13b
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeans.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2002-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.bean.override.mockito;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Container for {@link MockitoBean @MockitoBean} annotations which allows
+ * {@code @MockitoBean} to be used as a {@linkplain java.lang.annotation.Repeatable
+ * repeatable annotation} at the type level — for example, on test classes
+ * or interfaces implemented by test classes.
+ *
+ * @author Sam Brannen
+ * @since 6.2.2
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface MockitoBeans {
+
+ MockitoBean[] value();
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideTestUtils.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideTestUtils.java
index 8932a072c84d..994ffffa35d1 100644
--- a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideTestUtils.java
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideTestUtils.java
@@ -30,4 +30,8 @@ public static List findHandlers(Class> testClass) {
return BeanOverrideHandler.forTestClass(testClass);
}
+ public static List findAllHandlers(Class> testClass) {
+ return BeanOverrideHandler.findAllHandlers(testClass);
+ }
+
}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandlerTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandlerTests.java
index 72fc58149dad..466bcd93e348 100644
--- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandlerTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandlerTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -67,6 +67,76 @@ void isEqualToWithSameMetadataFromField() {
assertThat(handler1).hasSameHashCodeAs(handler2);
}
+ @Test // gh-33925
+ void isEqualToWithSameInstanceFromClassLevel() {
+ MockitoBeanOverrideHandler handler1 = createHandler(ClassLevelStringMockByName1.class);
+ assertThat(handler1).isEqualTo(handler1);
+ assertThat(handler1).hasSameHashCodeAs(handler1);
+
+ MockitoBeanOverrideHandler handler2 = createHandler(ClassLevelStringMockByType1.class);
+ assertThat(handler2).isEqualTo(handler2);
+ assertThat(handler2).hasSameHashCodeAs(handler2);
+ }
+
+ @Test // gh-33925
+ void isEqualToWithSameByNameLookupMetadataFromClassLevel() {
+ MockitoBeanOverrideHandler handler1 = createHandler(ClassLevelStringMockByName1.class);
+ MockitoBeanOverrideHandler handler2 = createHandler(ClassLevelStringMockByName2.class);
+ assertThat(handler1).isEqualTo(handler2);
+ assertThat(handler2).isEqualTo(handler1);
+ assertThat(handler1).hasSameHashCodeAs(handler2);
+ }
+
+ @Test // gh-33925
+ void isNotEqualToWithDifferentByNameLookupMetadataFromClassLevel() {
+ MockitoBeanOverrideHandler handler1 = createHandler(ClassLevelStringMockByName1.class);
+ MockitoBeanOverrideHandler handler2 = createHandler(ClassLevelStringMockByName3.class);
+ assertThat(handler1).isNotEqualTo(handler2);
+ assertThat(handler2).isNotEqualTo(handler1);
+ assertThat(handler1).doesNotHaveSameHashCodeAs(handler2);
+ }
+
+ @Test // gh-33925
+ void isEqualToWithSameByTypeLookupMetadataFromClassLevel() {
+ MockitoBeanOverrideHandler handler1 = createHandler(ClassLevelStringMockByType1.class);
+ MockitoBeanOverrideHandler handler2 = createHandler(ClassLevelStringMockByType2.class);
+ assertThat(handler1).isEqualTo(handler2);
+ assertThat(handler2).isEqualTo(handler1);
+ assertThat(handler1).hasSameHashCodeAs(handler2);
+ }
+
+ @Test // gh-33925
+ void isNotEqualToWithDifferentByTypeLookupMetadataFromClassLevel() {
+ MockitoBeanOverrideHandler handler1 = createHandler(ClassLevelStringMockByType1.class);
+ MockitoBeanOverrideHandler handler2 = createHandler(ClassLevelStringMockByType3.class);
+ assertThat(handler1).isNotEqualTo(handler2);
+ assertThat(handler2).isNotEqualTo(handler1);
+ assertThat(handler1).doesNotHaveSameHashCodeAs(handler2);
+ }
+
+ @Test // gh-33925
+ void isEqualToWithSameByNameLookupMetadataFromFieldAndClassLevel() {
+ MockitoBeanOverrideHandler handler1 = createHandler(sampleField("service3"));
+ MockitoBeanOverrideHandler handler2 = createHandler(ClassLevelStringMockByName1.class);
+ assertThat(handler1).isEqualTo(handler2);
+ assertThat(handler2).isEqualTo(handler1);
+ assertThat(handler1).hasSameHashCodeAs(handler2);
+ }
+
+ /**
+ * Since the "field name as fallback qualifier" is not available for an annotated class,
+ * what would seem to be "equivalent" handlers are actually not considered "equal" when
+ * the the lookup is "by type".
+ */
+ @Test // gh-33925
+ void isNotEqualToWithSameByTypeLookupMetadataFromFieldAndClassLevel() {
+ MockitoBeanOverrideHandler handler1 = createHandler(sampleField("service"));
+ MockitoBeanOverrideHandler handler2 = createHandler(ClassLevelStringMockByType1.class);
+ assertThat(handler1).isNotEqualTo(handler2);
+ assertThat(handler2).isNotEqualTo(handler1);
+ assertThat(handler1).doesNotHaveSameHashCodeAs(handler2);
+ }
+
@Test
void isNotEqualEqualToByTypeLookupWithSameMetadataButDifferentField() {
MockitoBeanOverrideHandler handler1 = createHandler(sampleField("service"));
@@ -122,6 +192,11 @@ private static MockitoBeanOverrideHandler createHandler(Field field) {
return new MockitoBeanOverrideHandler(field, ResolvableType.forClass(field.getType()), annotation);
}
+ private MockitoBeanOverrideHandler createHandler(Class> clazz) {
+ MockitoBean annotation = AnnotatedElementUtils.getMergedAnnotation(clazz, MockitoBean.class);
+ return new MockitoBeanOverrideHandler(null, ResolvableType.forClass(annotation.types()[0]), annotation);
+ }
+
static class SampleOneMock {
@@ -159,4 +234,28 @@ static class Sample {
private String service7;
}
+ @MockitoBean(name = "beanToMock", types = String.class)
+ static class ClassLevelStringMockByName1 {
+ }
+
+ @MockitoBean(name = "beanToMock", types = String.class)
+ static class ClassLevelStringMockByName2 {
+ }
+
+ @MockitoBean(name = "otherBeanToMock", types = String.class)
+ static class ClassLevelStringMockByName3 {
+ }
+
+ @MockitoBean(types = String.class)
+ static class ClassLevelStringMockByType1 {
+ }
+
+ @MockitoBean(types = String.class)
+ static class ClassLevelStringMockByType2 {
+ }
+
+ @MockitoBean(types = Integer.class)
+ static class ClassLevelStringMockByType3 {
+ }
+
}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessorTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessorTests.java
index d25475990b45..7f4d07c25c44 100644
--- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessorTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessorTests.java
@@ -18,7 +18,9 @@
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
+import java.util.List;
+import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.core.annotation.AnnotationUtils;
@@ -41,43 +43,155 @@ class MockitoBeanOverrideProcessorTests {
private final MockitoBeanOverrideProcessor processor = new MockitoBeanOverrideProcessor();
- private final Field field = ReflectionUtils.findField(TestCase.class, "number");
+ @Nested
+ class CreateHandlerTests {
+ private final Field field = ReflectionUtils.findField(TestCase.class, "number");
- @Test
- void mockAnnotationCreatesMockitoBeanOverrideHandler() {
- MockitoBean annotation = AnnotationUtils.synthesizeAnnotation(MockitoBean.class);
- BeanOverrideHandler object = processor.createHandler(annotation, TestCase.class, field);
- assertThat(object).isExactlyInstanceOf(MockitoBeanOverrideHandler.class);
- }
+ @Test
+ void mockAnnotationCreatesMockitoBeanOverrideHandler() {
+ MockitoBean annotation = AnnotationUtils.synthesizeAnnotation(MockitoBean.class);
+ BeanOverrideHandler object = processor.createHandler(annotation, TestCase.class, field);
- @Test
- void spyAnnotationCreatesMockitoSpyBeanOverrideHandler() {
- MockitoSpyBean annotation = AnnotationUtils.synthesizeAnnotation(MockitoSpyBean.class);
- BeanOverrideHandler object = processor.createHandler(annotation, TestCase.class, field);
+ assertThat(object).isExactlyInstanceOf(MockitoBeanOverrideHandler.class);
+ }
- assertThat(object).isExactlyInstanceOf(MockitoSpyBeanOverrideHandler.class);
- }
+ @Test
+ void spyAnnotationCreatesMockitoSpyBeanOverrideHandler() {
+ MockitoSpyBean annotation = AnnotationUtils.synthesizeAnnotation(MockitoSpyBean.class);
+ BeanOverrideHandler object = processor.createHandler(annotation, TestCase.class, field);
+
+ assertThat(object).isExactlyInstanceOf(MockitoSpyBeanOverrideHandler.class);
+ }
+
+ @Test
+ void otherAnnotationThrows() {
+ Annotation annotation = field.getAnnotation(Nullable.class);
+
+ assertThatIllegalStateException()
+ .isThrownBy(() -> processor.createHandler(annotation, TestCase.class, field))
+ .withMessage("Invalid annotation passed to MockitoBeanOverrideProcessor: expected either " +
+ "@MockitoBean or @MockitoSpyBean on field %s.%s", field.getDeclaringClass().getName(),
+ field.getName());
+ }
+
+ @Test
+ void typesNotSupportedAtFieldLevel() {
+ Field field = ReflectionUtils.findField(TestCase.class, "typesNotSupported");
+ MockitoBean annotation = field.getAnnotation(MockitoBean.class);
+
+ assertThatIllegalStateException()
+ .isThrownBy(() -> processor.createHandler(annotation, TestCase.class, field))
+ .withMessage("The @MockitoBean 'types' attribute must be omitted when declared on a field");
+ }
- @Test
- void otherAnnotationThrows() {
- Annotation annotation = field.getAnnotation(Nullable.class);
- assertThatIllegalStateException()
- .isThrownBy(() -> processor.createHandler(annotation, TestCase.class, field))
- .withMessage("Invalid annotation passed to MockitoBeanOverrideProcessor: expected either " +
- "@MockitoBean or @MockitoSpyBean on field %s.%s", field.getDeclaringClass().getName(),
- field.getName());
+ static class TestCase {
+
+ @Nullable
+ @MockitoBean
+ @MockitoSpyBean
+ Integer number;
+
+ @MockitoBean(types = Integer.class)
+ String typesNotSupported;
+ }
+
+ @MockitoBean(name = "bogus", types = Integer.class)
+ static class NameNotSupportedTestCase {
+ }
}
+ @Nested
+ class CreateHandlersTests {
+
+ @Test
+ void missingTypes() {
+ Class> testClass = MissingTypesTestCase.class;
+ MockitoBean annotation = testClass.getAnnotation(MockitoBean.class);
+
+ assertThatIllegalStateException()
+ .isThrownBy(() -> processor.createHandlers(annotation, testClass))
+ .withMessage("The @MockitoBean 'types' attribute must not be empty when declared on a class");
+ }
+
+ @Test
+ void nameNotSupportedWithMultipleTypes() {
+ Class> testClass = NameNotSupportedWithMultipleTypesTestCase.class;
+ MockitoBean annotation = testClass.getAnnotation(MockitoBean.class);
+
+ assertThatIllegalStateException()
+ .isThrownBy(() -> processor.createHandlers(annotation, testClass))
+ .withMessage("The @MockitoBean 'name' attribute cannot be used when mocking multiple types");
+ }
+
+ @Test
+ void singleMockByType() {
+ Class> testClass = SingleMockByTypeTestCase.class;
+ MockitoBean annotation = testClass.getAnnotation(MockitoBean.class);
+ List handlers = processor.createHandlers(annotation, testClass);
+
+ assertThat(handlers).singleElement().isInstanceOfSatisfying(MockitoBeanOverrideHandler.class, handler -> {
+ assertThat(handler.getField()).isNull();
+ assertThat(handler.getBeanName()).isNull();
+ assertThat(handler.getBeanType().resolve()).isEqualTo(Integer.class);
+ });
+ }
+
+ @Test
+ void singleMockByName() {
+ Class> testClass = SingleMockByNameTestCase.class;
+ MockitoBean annotation = testClass.getAnnotation(MockitoBean.class);
+ List handlers = processor.createHandlers(annotation, testClass);
+
+ assertThat(handlers).singleElement().isInstanceOfSatisfying(MockitoBeanOverrideHandler.class, handler -> {
+ assertThat(handler.getField()).isNull();
+ assertThat(handler.getBeanName()).isEqualTo("enigma");
+ assertThat(handler.getBeanType().resolve()).isEqualTo(Integer.class);
+ });
+ }
+
+ @Test
+ void multipleMocks() {
+ Class> testClass = MultipleMocksTestCase.class;
+ MockitoBean annotation = testClass.getAnnotation(MockitoBean.class);
+ List handlers = processor.createHandlers(annotation, testClass);
+
+ assertThat(handlers).satisfiesExactly(
+ handler1 -> {
+ assertThat(handler1.getField()).isNull();
+ assertThat(handler1.getBeanName()).isNull();
+ assertThat(handler1.getBeanType().resolve()).isEqualTo(Integer.class);
+ },
+ handler2 -> {
+ assertThat(handler2.getField()).isNull();
+ assertThat(handler2.getBeanName()).isNull();
+ assertThat(handler2.getBeanType().resolve()).isEqualTo(Float.class);
+ }
+ );
+ }
- static class TestCase {
- @Nullable
@MockitoBean
- @MockitoSpyBean
- Integer number;
+ static class MissingTypesTestCase {
+ }
+
+ @MockitoBean(name = "bogus", types = { Integer.class, Float.class })
+ static class NameNotSupportedWithMultipleTypesTestCase {
+ }
+
+ @MockitoBean(types = Integer.class)
+ static class SingleMockByTypeTestCase {
+ }
+
+ @MockitoBean(name = "enigma", types = Integer.class)
+ static class SingleMockByNameTestCase {
+ }
+
+ @MockitoBean(types = { Integer.class, Float.class })
+ static class MultipleMocksTestCase {
+ }
}
}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/MockitoBeansByNameIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/MockitoBeansByNameIntegrationTests.java
new file mode 100644
index 000000000000..b27e2dd860af
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/MockitoBeansByNameIntegrationTests.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2002-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.bean.override.mockito.mockbeans;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.bean.override.example.ExampleService;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import org.springframework.test.context.bean.override.mockito.MockitoBeans;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.BDDMockito.given;
+
+/**
+ * Integration tests for {@link MockitoBeans @MockitoBeans} and
+ * {@link MockitoBean @MockitoBean} declared "by name" at the class level as a
+ * repeatable annotation.
+ *
+ * @author Sam Brannen
+ * @since 6.2.2
+ * @see gh-33925
+ * @see MockitoBeansByTypeIntegrationTests
+ */
+@SpringJUnitConfig
+@MockitoBean(name = "s1", types = ExampleService.class)
+@MockitoBean(name = "s2", types = ExampleService.class)
+class MockitoBeansByNameIntegrationTests {
+
+ @Autowired
+ ExampleService s1;
+
+ @Autowired
+ ExampleService s2;
+
+ @MockitoBean(name = "s3")
+ ExampleService service3;
+
+ @Autowired
+ @Qualifier("s4")
+ ExampleService service4;
+
+
+ @BeforeEach
+ void configureMocks() {
+ given(s1.greeting()).willReturn("mock 1");
+ given(s2.greeting()).willReturn("mock 2");
+ given(service3.greeting()).willReturn("mock 3");
+ }
+
+ @Test
+ void checkMocksAndStandardBean() {
+ assertThat(s1.greeting()).isEqualTo("mock 1");
+ assertThat(s2.greeting()).isEqualTo("mock 2");
+ assertThat(service3.greeting()).isEqualTo("mock 3");
+ assertThat(service4.greeting()).isEqualTo("prod 4");
+ }
+
+ @Configuration
+ static class Config {
+
+ @Bean
+ ExampleService s1() {
+ return () -> "prod 1";
+ }
+
+ @Bean
+ ExampleService s2() {
+ return () -> "prod 2";
+ }
+
+ @Bean
+ ExampleService s3() {
+ return () -> "prod 3";
+ }
+
+ @Bean
+ ExampleService s4() {
+ return () -> "prod 4";
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/MockitoBeansByTypeIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/MockitoBeansByTypeIntegrationTests.java
new file mode 100644
index 000000000000..c70fc89f6120
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/MockitoBeansByTypeIntegrationTests.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2002-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.bean.override.mockito.mockbeans;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import org.springframework.test.context.bean.override.mockito.MockitoBeans;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.BDDMockito.given;
+
+/**
+ * Integration tests for {@link MockitoBeans @MockitoBeans} and
+ * {@link MockitoBean @MockitoBean} declared "by type" at the class level, as a
+ * repeatable annotation, and via a custom composed annotation.
+ *
+ * @author Sam Brannen
+ * @since 6.2.2
+ * @see gh-33925
+ * @see MockitoBeansByNameIntegrationTests
+ */
+@SpringJUnitConfig
+@MockitoBean(types = {Service04.class, Service05.class})
+@SharedMocks // Intentionally declared between local @MockitoBean declarations
+@MockitoBean(types = Service06.class)
+class MockitoBeansByTypeIntegrationTests implements TestInterface01 {
+
+ @Autowired
+ Service01 service01;
+
+ @Autowired
+ Service02 service02;
+
+ @Autowired
+ Service03 service03;
+
+ @Autowired
+ Service04 service04;
+
+ @Autowired
+ Service05 service05;
+
+ @Autowired
+ Service06 service06;
+
+ @MockitoBean
+ Service07 service07;
+
+
+ @BeforeEach
+ void configureMocks() {
+ given(service01.greeting()).willReturn("mock 01");
+ given(service02.greeting()).willReturn("mock 02");
+ given(service03.greeting()).willReturn("mock 03");
+ given(service04.greeting()).willReturn("mock 04");
+ given(service05.greeting()).willReturn("mock 05");
+ given(service06.greeting()).willReturn("mock 06");
+ given(service07.greeting()).willReturn("mock 07");
+ }
+
+ @Test
+ void checkMocks() {
+ assertThat(service01.greeting()).isEqualTo("mock 01");
+ assertThat(service02.greeting()).isEqualTo("mock 02");
+ assertThat(service03.greeting()).isEqualTo("mock 03");
+ assertThat(service04.greeting()).isEqualTo("mock 04");
+ assertThat(service05.greeting()).isEqualTo("mock 05");
+ assertThat(service06.greeting()).isEqualTo("mock 06");
+ assertThat(service07.greeting()).isEqualTo("mock 07");
+ }
+
+
+ @MockitoBean(types = Service09.class)
+ static class BaseTestCase implements TestInterface08 {
+
+ @Autowired
+ Service08 service08;
+
+ @Autowired
+ Service09 service09;
+
+ @MockitoBean
+ Service10 service10;
+ }
+
+ @Nested
+ @MockitoBean(types = Service12.class)
+ class NestedTests extends BaseTestCase implements TestInterface11 {
+
+ @Autowired
+ Service11 service11;
+
+ @Autowired
+ Service12 service12;
+
+ @MockitoBean
+ Service13 service13;
+
+
+ @BeforeEach
+ void configureMocks() {
+ given(service08.greeting()).willReturn("mock 08");
+ given(service09.greeting()).willReturn("mock 09");
+ given(service10.greeting()).willReturn("mock 10");
+ given(service11.greeting()).willReturn("mock 11");
+ given(service12.greeting()).willReturn("mock 12");
+ given(service13.greeting()).willReturn("mock 13");
+ }
+
+ @Test
+ void checkMocks() {
+ assertThat(service01.greeting()).isEqualTo("mock 01");
+ assertThat(service02.greeting()).isEqualTo("mock 02");
+ assertThat(service03.greeting()).isEqualTo("mock 03");
+ assertThat(service04.greeting()).isEqualTo("mock 04");
+ assertThat(service05.greeting()).isEqualTo("mock 05");
+ assertThat(service06.greeting()).isEqualTo("mock 06");
+ assertThat(service07.greeting()).isEqualTo("mock 07");
+ assertThat(service08.greeting()).isEqualTo("mock 08");
+ assertThat(service09.greeting()).isEqualTo("mock 09");
+ assertThat(service10.greeting()).isEqualTo("mock 10");
+ assertThat(service11.greeting()).isEqualTo("mock 11");
+ assertThat(service12.greeting()).isEqualTo("mock 12");
+ assertThat(service13.greeting()).isEqualTo("mock 13");
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/MockitoBeansTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/MockitoBeansTests.java
new file mode 100644
index 000000000000..3ef3e14ea2e2
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/MockitoBeansTests.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2002-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.bean.override.mockito.mockbeans;
+
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.core.ResolvableType;
+import org.springframework.test.context.bean.override.BeanOverrideHandler;
+import org.springframework.test.context.bean.override.BeanOverrideTestUtils;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import org.springframework.test.context.bean.override.mockito.MockitoBeans;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link MockitoBeans @MockitoBeans}: {@link MockitoBean @MockitoBean}
+ * declared at the class level, as a repeatable annotation, and via a custom composed
+ * annotation.
+ *
+ * @author Sam Brannen
+ * @since 6.2.2
+ * @see gh-33925
+ */
+class MockitoBeansTests {
+
+ @Test
+ void registrationOrderForTopLevelClass() {
+ Stream> mockedServices = getRegisteredMockTypes(MockitoBeansByTypeIntegrationTests.class);
+ assertThat(mockedServices).containsExactly(
+ Service01.class, Service02.class, Service03.class, Service04.class,
+ Service05.class, Service06.class, Service07.class);
+ }
+
+ @Test
+ void registrationOrderForNestedClass() {
+ Stream> mockedServices = getRegisteredMockTypes(MockitoBeansByTypeIntegrationTests.NestedTests.class);
+ assertThat(mockedServices).containsExactly(
+ Service01.class, Service02.class, Service03.class, Service04.class,
+ Service05.class, Service06.class, Service07.class, Service08.class,
+ Service09.class, Service10.class, Service11.class, Service12.class,
+ Service13.class);
+ }
+
+
+ private static Stream> getRegisteredMockTypes(Class> testClass) {
+ return BeanOverrideTestUtils.findAllHandlers(testClass)
+ .stream()
+ .map(BeanOverrideHandler::getBeanType)
+ .map(ResolvableType::getRawClass);
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service.java
new file mode 100644
index 000000000000..187ffeb6a833
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2002-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.bean.override.mockito.mockbeans;
+
+interface Service {
+ String greeting();
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service01.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service01.java
new file mode 100644
index 000000000000..a2110bf7fb5a
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service01.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2002-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.bean.override.mockito.mockbeans;
+
+interface Service01 extends Service {
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service02.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service02.java
new file mode 100644
index 000000000000..c2b62a558eec
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service02.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2002-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.bean.override.mockito.mockbeans;
+
+interface Service02 extends Service {
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service03.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service03.java
new file mode 100644
index 000000000000..31fe690f5944
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service03.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2002-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.bean.override.mockito.mockbeans;
+
+interface Service03 extends Service {
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service04.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service04.java
new file mode 100644
index 000000000000..d32ba233d478
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service04.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2002-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.bean.override.mockito.mockbeans;
+
+interface Service04 extends Service {
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service05.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service05.java
new file mode 100644
index 000000000000..8c738aa36dde
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service05.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2002-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.bean.override.mockito.mockbeans;
+
+interface Service05 extends Service {
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service06.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service06.java
new file mode 100644
index 000000000000..bceab3996501
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service06.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2002-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.bean.override.mockito.mockbeans;
+
+interface Service06 extends Service {
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service07.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service07.java
new file mode 100644
index 000000000000..35e82c9fdad8
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service07.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2002-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.bean.override.mockito.mockbeans;
+
+interface Service07 extends Service {
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service08.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service08.java
new file mode 100644
index 000000000000..d9630716cf73
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service08.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2002-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.bean.override.mockito.mockbeans;
+
+interface Service08 extends Service {
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service09.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service09.java
new file mode 100644
index 000000000000..cb5e92242eee
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service09.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2002-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.bean.override.mockito.mockbeans;
+
+interface Service09 extends Service {
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service10.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service10.java
new file mode 100644
index 000000000000..50231a138491
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service10.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2002-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.bean.override.mockito.mockbeans;
+
+interface Service10 extends Service {
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service11.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service11.java
new file mode 100644
index 000000000000..3b9203d5ee88
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service11.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2002-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.bean.override.mockito.mockbeans;
+
+interface Service11 extends Service {
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service12.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service12.java
new file mode 100644
index 000000000000..097d5ade6c47
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service12.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2002-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.bean.override.mockito.mockbeans;
+
+interface Service12 extends Service {
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service13.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service13.java
new file mode 100644
index 000000000000..233e52be6f25
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service13.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2002-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.bean.override.mockito.mockbeans;
+
+interface Service13 extends Service {
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/SharedMocks.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/SharedMocks.java
new file mode 100644
index 000000000000..e44017dc6af1
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/SharedMocks.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2002-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.bean.override.mockito.mockbeans;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@MockitoBean(types = Service02.class)
+@MockitoBean(types = Service03.class)
+@interface SharedMocks {
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/TestInterface01.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/TestInterface01.java
new file mode 100644
index 000000000000..a28e7d495984
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/TestInterface01.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2002-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.bean.override.mockito.mockbeans;
+
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+
+@MockitoBean(types = Service01.class)
+interface TestInterface01 {
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/TestInterface08.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/TestInterface08.java
new file mode 100644
index 000000000000..45d326882210
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/TestInterface08.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2002-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.bean.override.mockito.mockbeans;
+
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+
+@MockitoBean(types = Service08.class)
+interface TestInterface08 {
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/TestInterface11.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/TestInterface11.java
new file mode 100644
index 000000000000..2bfe47e028b4
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/TestInterface11.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2002-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.bean.override.mockito.mockbeans;
+
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+
+@MockitoBean(types = Service11.class)
+interface TestInterface11 {
+}