From e31ce359a17a1e55cacad6f4da16670bd6a7817e Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Tue, 11 Feb 2025 17:46:01 +0100 Subject: [PATCH] =?UTF-8?q?Support=20@=E2=81=A0MockitoSpyBean=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, @⁠MockitoSpyBean could only be declared on fields within test classes, which prevented developers from being able to easily reuse spy configuration across a test suite. With this commit, @⁠MockitoSpyBean is now supported at the type level on test classes, their superclasses, and interfaces implemented by those classes. @⁠MockitoSpyBean is also supported on enclosing classes for @⁠Nested test classes, their superclasses, and interfaces implemented by those classes, while honoring @⁠NestedTestConfiguration semantics. In addition, @⁠MockitoSpyBean: - has a new `types` attribute that can be used to declare the type or types to spy when @⁠MockitoSpyBean 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 @⁠SharedSpies example in the reference manual) To support these new features, this commit also includes the following changes. - MockitoSpyBeanOverrideProcessor has been revised to support @⁠MockitoSpyBean at the type level. - The "Bean Overriding in Tests" and "@⁠MockitoBean and @⁠MockitoSpyBean" sections of the reference manual have been fully revised. See gh-34408 Closes gh-33925 --- .../annotation-mockitobean.adoc | 158 ++++++--- .../bean-overriding.adoc | 13 +- .../bean/override/mockito/MockitoBean.java | 29 +- .../mockito/MockitoBeanOverrideProcessor.java | 45 ++- .../bean/override/mockito/MockitoSpyBean.java | 77 ++++- .../MockitoSpyBeanOverrideHandler.java | 6 +- .../override/mockito/MockitoSpyBeans.java | 41 +++ .../MockitoBeanOverrideProcessorTests.java | 244 ++++++++++---- .../MockTestInterface01.java} | 4 +- .../MockTestInterface08.java} | 4 +- .../MockTestInterface11.java} | 4 +- .../MockitoBeansByNameIntegrationTests.java | 10 +- .../MockitoBeansByTypeIntegrationTests.java | 31 +- .../MockitoBeansTests.java | 2 +- ...MockitoSpyBeansByNameIntegrationTests.java | 127 ++++++++ ...MockitoSpyBeansByTypeIntegrationTests.java | 307 ++++++++++++++++++ .../typelevel/MockitoSpyBeansTests.java | 68 ++++ .../{mockbeans => typelevel}/Service.java | 2 +- .../{mockbeans => typelevel}/Service01.java | 2 +- .../{mockbeans => typelevel}/Service02.java | 2 +- .../{mockbeans => typelevel}/Service03.java | 2 +- .../{mockbeans => typelevel}/Service04.java | 2 +- .../{mockbeans => typelevel}/Service05.java | 2 +- .../{mockbeans => typelevel}/Service06.java | 2 +- .../{mockbeans => typelevel}/Service07.java | 2 +- .../{mockbeans => typelevel}/Service08.java | 2 +- .../{mockbeans => typelevel}/Service09.java | 2 +- .../{mockbeans => typelevel}/Service10.java | 2 +- .../{mockbeans => typelevel}/Service11.java | 2 +- .../{mockbeans => typelevel}/Service12.java | 2 +- .../{mockbeans => typelevel}/Service13.java | 2 +- .../{mockbeans => typelevel}/SharedMocks.java | 2 +- .../mockito/typelevel/SharedSpies.java | 31 ++ .../mockito/typelevel/SpyTestInterface01.java | 23 ++ .../mockito/typelevel/SpyTestInterface08.java | 23 ++ .../mockito/typelevel/SpyTestInterface11.java | 23 ++ .../test/mockito/MockitoAssertions.java | 2 + 37 files changed, 1100 insertions(+), 202 deletions(-) create mode 100644 spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeans.java rename spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/{mockbeans/TestInterface01.java => typelevel/MockTestInterface01.java} (87%) rename spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/{mockbeans/TestInterface08.java => typelevel/MockTestInterface08.java} (87%) rename spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/{mockbeans/TestInterface11.java => typelevel/MockTestInterface11.java} (87%) rename spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/{mockbeans => typelevel}/MockitoBeansByNameIntegrationTests.java (88%) rename spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/{mockbeans => typelevel}/MockitoBeansByTypeIntegrationTests.java (78%) rename spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/{mockbeans => typelevel}/MockitoBeansTests.java (97%) create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/MockitoSpyBeansByNameIntegrationTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/MockitoSpyBeansByTypeIntegrationTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/MockitoSpyBeansTests.java rename spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/{mockbeans => typelevel}/Service.java (90%) rename spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/{mockbeans => typelevel}/Service01.java (89%) rename spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/{mockbeans => typelevel}/Service02.java (89%) rename spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/{mockbeans => typelevel}/Service03.java (89%) rename spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/{mockbeans => typelevel}/Service04.java (89%) rename spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/{mockbeans => typelevel}/Service05.java (89%) rename spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/{mockbeans => typelevel}/Service06.java (89%) rename spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/{mockbeans => typelevel}/Service07.java (89%) rename spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/{mockbeans => typelevel}/Service08.java (89%) rename spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/{mockbeans => typelevel}/Service09.java (89%) rename spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/{mockbeans => typelevel}/Service10.java (89%) rename spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/{mockbeans => typelevel}/Service11.java (89%) rename spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/{mockbeans => typelevel}/Service12.java (89%) rename spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/{mockbeans => typelevel}/Service13.java (89%) rename spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/{mockbeans => typelevel}/SharedMocks.java (93%) create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/SharedSpies.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/SpyTestInterface01.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/SpyTestInterface08.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/SpyTestInterface11.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 6bb17af198d4..8af25b975518 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 @@ -1,34 +1,60 @@ [[spring-testing-annotation-beanoverriding-mockitobean]] = `@MockitoBean` and `@MockitoSpyBean` -`@MockitoBean` and `@MockitoSpyBean` are used on non-static fields in test classes to -override beans in the test's `ApplicationContext` with a Mockito _mock_ or _spy_, -respectively. In the latter case, an early instance of the original bean is captured and -wrapped by the spy. +`@MockitoBean` and `@MockitoSpyBean` can be used in test classes to override a bean in +the test's `ApplicationContext` with a Mockito _mock_ or _spy_, respectively. In the +latter case, an early instance of the original bean is captured and wrapped by the spy. + +The annotations 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 `@Nested` test class or in any class + in the type hierarchy or enclosing class hierarchy above the `@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 `@Nested` test class or on any class or + interface in the type hierarchy or enclosing class hierarchy above the `@Nested` test + class. + +When `@MockitoBean` or `@MockitoSpyBean` is declared on a field, the bean to mock or spy +is inferred from the type of the annotated field. If multiple candidates exist in the +`ApplicationContext`, a `@Qualifier` annotation can be declared on the field to help +disambiguate. In the absence of a `@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 or spy by setting the `value` or `name` attribute in the annotation. + +When `@MockitoBean` or `@MockitoSpyBean` is declared at the type level, the type of bean +(or beans) to mock or spy must be supplied via the `types` attribute in the annotation – +for example, `@MockitoBean(types = {OrderService.class, UserService.class})`. If multiple +candidates exist in the `ApplicationContext`, you can explicitly specify a bean name to +mock or spy 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)`. -By default, the annotated field's type is used to search for candidate beans to override. -If multiple candidates match, `@Qualifier` can be provided to narrow the candidate to -override. Alternatively, a candidate whose bean name matches the name of the field will -match. +To support reuse of mock configuration, `@MockitoBean` and `@MockitoSpyBean` may be used +as meta-annotations to create custom _composed annotations_ – for example, to define +common mock or spy configuration in a single annotation that can be reused across a test +suite. `@MockitoBean` and `@MockitoSpyBean` can also be used as repeatable annotations at +the type level — for example, to mock or spy several beans by name. [WARNING] ==== -Qualifiers, including the name of the field, are used to determine if a separate +Qualifiers, including the name of a 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 test classes, make sure to name the field consistently to avoid +the same bean in several test classes, make sure to name the fields consistently to avoid creating unnecessary contexts. ==== Each annotation also defines Mockito-specific attributes to fine-tune the mocking behavior. 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. However, you can switch to -the `REPLACE` strategy by setting the `enforceOverride` attribute to `true`. See the -following section for an example. +xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-strategy[strategy for bean overrides]. +If a corresponding bean does not exist, a new bean will be created. However, you can +switch to the `REPLACE` strategy by setting the `enforceOverride` attribute to `true` – +for example, `@MockitoBean(enforceOverride = true)`. The `@MockitoSpyBean` annotation uses the `WRAP` -xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy], +xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-strategy[strategy], and the original instance is wrapped in a Mockito spy. This strategy requires that exactly one candidate bean exists. @@ -56,15 +82,8 @@ 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: +The following example shows how to use the default behavior of the `@MockitoBean` +annotation. [tabs] ====== @@ -81,7 +100,7 @@ Java:: // tests... } ---- -<1> Replace the bean with type `CustomService` with a Mockito `mock`. +<1> Replace the bean with type `CustomService` with a Mockito mock. ====== In the example above, we are creating a mock for `CustomService`. If more than one bean @@ -90,7 +109,8 @@ will fail, and you will need to provide a qualifier of some sort to identify whi `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: +The following example uses a by-name lookup, rather than a by-type lookup. If no bean +named `service` exists, one is created. [tabs] ====== @@ -108,32 +128,9 @@ Java:: } ---- -<1> Replace the bean named `service` with a Mockito `mock`. +<1> Replace the bean named `service` with a Mockito mock. ====== -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 - -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] @@ -191,7 +188,7 @@ APIs. == `@MockitoSpyBean` Examples The following example shows how to use the default behavior of the `@MockitoSpyBean` -annotation: +annotation. [tabs] ====== @@ -208,7 +205,7 @@ Java:: // tests... } ---- -<1> Wrap the bean with type `CustomService` with a Mockito `spy`. +<1> Wrap the bean with type `CustomService` with a Mockito spy. ====== In the example above, we are wrapping the bean with type `CustomService`. If more than @@ -216,7 +213,7 @@ one bean of that type exists, the bean named `customService` is considered. Othe 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 spy. -The following example uses a by-name lookup, rather than a by-type lookup: +The following example uses a by-name lookup, rather than a by-type lookup. [tabs] ====== @@ -233,5 +230,58 @@ Java:: // tests... } ---- -<1> Wrap the bean named `service` with a Mockito `spy`. +<1> Wrap the bean named `service` with a Mockito spy. +====== + +The following `@SharedSpies` annotation registers two spies by-type and one spy by-name. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @MockitoSpyBean(types = {OrderService.class, UserService.class}) // <1> + @MockitoSpyBean(name = "ps1", types = PrintingService.class) // <2> + public @interface SharedSpies { + } +---- +<1> Register `OrderService` and `UserService` spies by-type. +<2> Register `PrintingService` spy by-name. +====== + +The following demonstrates how `@SharedSpies` can be used on a test class. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig(TestConfig.class) + @SharedSpies // <1> + class BeanOverrideTests { + + @Autowired OrderService orderService; // <2> + + @Autowired UserService userService; // <2> + + @Autowired PrintingService ps1; // <2> + + // Inject other components that rely on the spies. + + @Test + void testThatDependsOnMocks() { + // ... + } + } +---- +<1> Register common spies via the custom `@SharedSpies` annotation. +<2> Optionally inject spies to _stub_ or _verify_ them. ====== + +TIP: The spies 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. diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/bean-overriding.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/bean-overriding.adoc index 52edc4a59947..a709dd96e432 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/bean-overriding.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/bean-overriding.adoc @@ -2,8 +2,8 @@ = Bean Overriding in Tests Bean overriding in tests refers to the ability to override specific beans in the -`ApplicationContext` for a test class, by annotating one or more non-static fields in the -test class. +`ApplicationContext` for a test class, by annotating the test class or one or more +non-static fields in the test class. NOTE: This feature is intended as a less risky alternative to the practice of registering a bean via `@Bean` with the `DefaultListableBeanFactory` @@ -42,15 +42,16 @@ The `spring-test` module registers implementations of the latter two {spring-framework-code}/spring-test/src/main/resources/META-INF/spring.factories[`META-INF/spring.factories` properties file]. -The bean overriding infrastructure searches in test classes for any non-static field that -is meta-annotated with `@BeanOverride` and instantiates the corresponding -`BeanOverrideProcessor` which is responsible for creating an appropriate -`BeanOverrideHandler`. +The bean overriding infrastructure searches for annotations on test classes as well as +annotations on non-static fields in test classes that are meta-annotated with +`@BeanOverride` and instantiates the corresponding `BeanOverrideProcessor` which is +responsible for creating an appropriate `BeanOverrideHandler`. The internal `BeanOverrideBeanFactoryPostProcessor` then uses bean override handlers to alter the test's `ApplicationContext` by creating, replacing, or wrapping beans as defined by the corresponding `BeanOverrideStrategy`: +[[testcontext-bean-overriding-strategy]] `REPLACE`:: Replaces the bean. Throws an exception if a corresponding bean does not exist. `REPLACE_OR_CREATE`:: 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 7d11bd813c22..46d5c0917f9c 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 @@ -31,9 +31,9 @@ /** * {@code @MockitoBean} is an annotation that can be used in test classes to - * override beans in a test's + * override a bean in the test's * {@link org.springframework.context.ApplicationContext ApplicationContext} - * using Mockito mocks. + * with a Mockito mock. * *

{@code @MockitoBean} can be applied in the following ways. *

* *

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. + * from the type of the annotated field. If multiple candidates exist in the + * {@code ApplicationContext}, 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. + * (or beans) to mock must be supplied via the {@link #types() types} attribute. + * If multiple candidates exist in the {@code ApplicationContext}, 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, @@ -111,7 +112,7 @@ public @interface MockitoBean { /** - * Alias for {@link #name()}. + * Alias for {@link #name() name}. *

Intended to be used when no other attributes are needed — for * example, {@code @MockitoBean("customBeanName")}. * @see #name() @@ -136,7 +137,7 @@ *

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 + *

When {@code @MockitoBean} also defines a {@link #name name}, this attribute * can only contain a single value. * @return the types to mock * @since 6.2.2 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 62c07d594153..7756dd720bdb 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 @@ -45,8 +45,10 @@ public AbstractMockitoBeanOverrideHandler createHandler(Annotation overrideAnnot "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) { - return new MockitoSpyBeanOverrideHandler(field, ResolvableType.forField(field, testClass), spyBean); + else if (overrideAnnotation instanceof MockitoSpyBean mockitoSpyBean) { + Assert.state(mockitoSpyBean.types().length == 0, + "The @MockitoSpyBean 'types' attribute must be omitted when declared on a field"); + return new MockitoSpyBeanOverrideHandler(field, ResolvableType.forField(field, testClass), mockitoSpyBean); } throw new IllegalStateException(""" Invalid annotation passed to MockitoBeanOverrideProcessor: \ @@ -56,21 +58,34 @@ else if (overrideAnnotation instanceof MockitoSpyBean spyBean) { @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()); + if (overrideAnnotation instanceof MockitoBean mockitoBean) { + 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; } - 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)); + else if (overrideAnnotation instanceof MockitoSpyBean mockitoSpyBean) { + Class[] types = mockitoSpyBean.types(); + Assert.state(types.length > 0, + "The @MockitoSpyBean 'types' attribute must not be empty when declared on a class"); + Assert.state(mockitoSpyBean.name().isEmpty() || types.length == 1, + "The @MockitoSpyBean 'name' attribute cannot be used when mocking multiple types"); + List handlers = new ArrayList<>(); + for (Class type : types) { + handlers.add(new MockitoSpyBeanOverrideHandler(ResolvableType.forClass(type), mockitoSpyBean)); + } + return handlers; } - return handlers; + throw new IllegalStateException(""" + Invalid annotation passed to MockitoBeanOverrideProcessor: \ + expected either @MockitoBean or @MockitoSpyBean on test class %s""" + .formatted(testClass.getName())); } } 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 d10674b75331..e42c0b4563ba 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 @@ -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; @@ -26,19 +27,40 @@ import org.springframework.test.context.bean.override.BeanOverride; /** - * {@code @MockitoSpyBean} 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 @MockitoSpyBean} is an annotation that can be used in test classes to + * override a bean in the test's * {@link org.springframework.context.ApplicationContext ApplicationContext} * with a Mockito spy that wraps the original bean instance. * - *

By default, the bean to spy 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 spy by setting the - * {@link #value() value} or {@link #name() name} attribute. If a bean name is - * specified, it is required that a target bean with that name has been previously - * registered in the application context. + *

{@code @MockitoSpyBean} can be applied in the following ways. + *

+ * + *

When {@code @MockitoSpyBean} is declared on a field, the bean to spy is + * inferred from the type of the annotated field. If multiple candidates exist in + * the {@code ApplicationContext}, 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 spy + * by setting the {@link #value() value} or {@link #name() name} attribute. If a + * bean name is specified, it is required that a target bean with that name has + * been previously registered in the application context. + * + *

When {@code @MockitoSpyBean} is declared at the type level, the type of bean + * (or beans) to spy must be supplied via the {@link #types() types} attribute. + * If multiple candidates exist in the {@code ApplicationContext}, you can + * explicitly specify a bean name to spy 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 spy cannot be created for components which are known to the application * context but are not beans — for example, components @@ -56,24 +78,33 @@ * (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} + *

{@code @MockitoSpyBean} fields and type-level {@code @MockitoSpyBean} declarations + * will be inherited from an enclosing test class by default. See + * {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration} * for details. * + *

{@code @MockitoSpyBean} may be used as a meta-annotation to create + * custom composed annotations — for example, to define common spy + * configuration in a single annotation that can be reused across a test suite. + * {@code @MockitoSpyBean} can also be used as a {@linkplain Repeatable repeatable} + * annotation at the type level — for example, to spy on several beans by + * {@link #name() name}. + * * @author Simon Baslé * @author Sam Brannen * @since 6.2 * @see org.springframework.test.context.bean.override.mockito.MockitoBean @MockitoBean * @see org.springframework.test.context.bean.override.convention.TestBean @TestBean */ -@Target(ElementType.FIELD) +@Target({ElementType.FIELD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented +@Repeatable(MockitoSpyBeans.class) @BeanOverride(MockitoBeanOverrideProcessor.class) public @interface MockitoSpyBean { /** - * Alias for {@link #name()}. + * Alias for {@link #name() name}. *

Intended to be used when no other attributes are needed — for * example, {@code @MockitoSpyBean("customBeanName")}. * @see #name() @@ -84,13 +115,27 @@ /** * Name of the bean to spy. *

If left unspecified, the bean to spy is selected according to the - * annotated field's type, taking qualifiers into account if necessary. See - * the {@linkplain MockitoSpyBean class-level documentation} for details. + * configured {@link #types() types} or the annotated field's type, taking + * qualifiers into account if necessary. See the {@linkplain MockitoSpyBean + * class-level documentation} for details. * @see #value() */ @AliasFor("value") String name() default ""; + /** + * One or more types to spy. + *

Defaults to none. + *

Each type specified will result in a spy being created and registered + * with the {@code ApplicationContext}. + *

Types must be omitted when the annotation is used on a field. + *

When {@code @MockitoSpyBean} also defines a {@link #name name}, this + * attribute can only contain a single value. + * @return the types to spy + * @since 6.2.3 + */ + Class[] types() default {}; + /** * The reset mode to apply to the spied bean. *

The default is {@link MockReset#AFTER} meaning that spies are automatically diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideHandler.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideHandler.java index 1adba89bb8cb..ce3f11cbe204 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideHandler.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideHandler.java @@ -48,7 +48,11 @@ class MockitoSpyBeanOverrideHandler extends AbstractMockitoBeanOverrideHandler { new SpringAopBypassingVerificationStartedListener(); - MockitoSpyBeanOverrideHandler(Field field, ResolvableType typeToSpy, MockitoSpyBean spyBean) { + MockitoSpyBeanOverrideHandler(ResolvableType typeToSpy, MockitoSpyBean spyBean) { + this(null, typeToSpy, spyBean); + } + + MockitoSpyBeanOverrideHandler(@Nullable Field field, ResolvableType typeToSpy, MockitoSpyBean spyBean) { super(field, typeToSpy, (StringUtils.hasText(spyBean.name()) ? spyBean.name() : null), BeanOverrideStrategy.WRAP, spyBean.reset()); Assert.notNull(typeToSpy, "typeToSpy must not be null"); diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeans.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeans.java new file mode 100644 index 000000000000..2482b96f2477 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeans.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 MockitoSpyBean @MockitoSpyBean} annotations which allows + * {@code @MockitoSpyBean} 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.3 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface MockitoSpyBeans { + + MockitoSpyBean[] value(); + +} 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 7f4d07c25c44..5ac9041ac6c5 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 @@ -107,91 +107,197 @@ static class NameNotSupportedTestCase { class CreateHandlersTests { @Test - void missingTypes() { - Class testClass = MissingTypesTestCase.class; - MockitoBean annotation = testClass.getAnnotation(MockitoBean.class); + void otherAnnotationThrows() { + Annotation annotation = getClass().getAnnotation(Nested.class); assertThatIllegalStateException() - .isThrownBy(() -> processor.createHandlers(annotation, testClass)) - .withMessage("The @MockitoBean 'types' attribute must not be empty when declared on a class"); + .isThrownBy(() -> processor.createHandlers(annotation, getClass())) + .withMessage("Invalid annotation passed to MockitoBeanOverrideProcessor: expected either " + + "@MockitoBean or @MockitoSpyBean on test class %s", getClass().getName()); } - @Test - void nameNotSupportedWithMultipleTypes() { - Class testClass = NameNotSupportedWithMultipleTypesTestCase.class; - MockitoBean annotation = testClass.getAnnotation(MockitoBean.class); + @Nested + class MockitoBeanTests { + + @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); + } + ); + } - 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); - }); - } + @MockitoBean + static class MissingTypesTestCase { + } - @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); - }); - } + @MockitoBean(name = "bogus", types = { Integer.class, Float.class }) + static class NameNotSupportedWithMultipleTypesTestCase { + } - @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); - } - ); - } + @MockitoBean(types = Integer.class) + static class SingleMockByTypeTestCase { + } + @MockitoBean(name = "enigma", types = Integer.class) + static class SingleMockByNameTestCase { + } - @MockitoBean - static class MissingTypesTestCase { + @MockitoBean(types = { Integer.class, Float.class }) + static class MultipleMocksTestCase { + } } - @MockitoBean(name = "bogus", types = { Integer.class, Float.class }) - static class NameNotSupportedWithMultipleTypesTestCase { - } + @Nested + class MockitoSpyBeanTests { + + @Test + void missingTypes() { + Class testClass = MissingTypesTestCase.class; + MockitoSpyBean annotation = testClass.getAnnotation(MockitoSpyBean.class); + + assertThatIllegalStateException() + .isThrownBy(() -> processor.createHandlers(annotation, testClass)) + .withMessage("The @MockitoSpyBean 'types' attribute must not be empty when declared on a class"); + } + + @Test + void nameNotSupportedWithMultipleTypes() { + Class testClass = NameNotSupportedWithMultipleTypesTestCase.class; + MockitoSpyBean annotation = testClass.getAnnotation(MockitoSpyBean.class); + + assertThatIllegalStateException() + .isThrownBy(() -> processor.createHandlers(annotation, testClass)) + .withMessage("The @MockitoSpyBean 'name' attribute cannot be used when mocking multiple types"); + } + + @Test + void singleSpyByType() { + Class testClass = SingleSpyByTypeTestCase.class; + MockitoSpyBean annotation = testClass.getAnnotation(MockitoSpyBean.class); + List handlers = processor.createHandlers(annotation, testClass); + + assertThat(handlers).singleElement().isInstanceOfSatisfying(MockitoSpyBeanOverrideHandler.class, handler -> { + assertThat(handler.getField()).isNull(); + assertThat(handler.getBeanName()).isNull(); + assertThat(handler.getBeanType().resolve()).isEqualTo(Integer.class); + }); + } + + @Test + void singleSpyByName() { + Class testClass = SingleSpyByNameTestCase.class; + MockitoSpyBean annotation = testClass.getAnnotation(MockitoSpyBean.class); + List handlers = processor.createHandlers(annotation, testClass); + + assertThat(handlers).singleElement().isInstanceOfSatisfying(MockitoSpyBeanOverrideHandler.class, handler -> { + assertThat(handler.getField()).isNull(); + assertThat(handler.getBeanName()).isEqualTo("enigma"); + assertThat(handler.getBeanType().resolve()).isEqualTo(Integer.class); + }); + } + + @Test + void multipleSpies() { + Class testClass = MultipleSpiesTestCase.class; + MockitoSpyBean annotation = testClass.getAnnotation(MockitoSpyBean.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); + } + ); + } - @MockitoBean(types = Integer.class) - static class SingleMockByTypeTestCase { - } - @MockitoBean(name = "enigma", types = Integer.class) - static class SingleMockByNameTestCase { - } + @MockitoSpyBean + static class MissingTypesTestCase { + } + + @MockitoSpyBean(name = "bogus", types = { Integer.class, Float.class }) + static class NameNotSupportedWithMultipleTypesTestCase { + } + + @MockitoSpyBean(types = Integer.class) + static class SingleSpyByTypeTestCase { + } - @MockitoBean(types = { Integer.class, Float.class }) - static class MultipleMocksTestCase { + @MockitoSpyBean(name = "enigma", types = Integer.class) + static class SingleSpyByNameTestCase { + } + + @MockitoSpyBean(types = { Integer.class, Float.class }) + static class MultipleSpiesTestCase { + } } + } } 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/typelevel/MockTestInterface01.java similarity index 87% rename from spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/TestInterface01.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/MockTestInterface01.java index a28e7d495984..394a04b9b116 100644 --- 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/typelevel/MockTestInterface01.java @@ -14,10 +14,10 @@ * limitations under the License. */ -package org.springframework.test.context.bean.override.mockito.mockbeans; +package org.springframework.test.context.bean.override.mockito.typelevel; import org.springframework.test.context.bean.override.mockito.MockitoBean; @MockitoBean(types = Service01.class) -interface TestInterface01 { +interface MockTestInterface01 { } 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/typelevel/MockTestInterface08.java similarity index 87% rename from spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/TestInterface08.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/MockTestInterface08.java index 45d326882210..4aeeced98230 100644 --- 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/typelevel/MockTestInterface08.java @@ -14,10 +14,10 @@ * limitations under the License. */ -package org.springframework.test.context.bean.override.mockito.mockbeans; +package org.springframework.test.context.bean.override.mockito.typelevel; import org.springframework.test.context.bean.override.mockito.MockitoBean; @MockitoBean(types = Service08.class) -interface TestInterface08 { +interface MockTestInterface08 { } 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/typelevel/MockTestInterface11.java similarity index 87% rename from spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/TestInterface11.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/MockTestInterface11.java index 2bfe47e028b4..1f8e1511b521 100644 --- 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/typelevel/MockTestInterface11.java @@ -14,10 +14,10 @@ * limitations under the License. */ -package org.springframework.test.context.bean.override.mockito.mockbeans; +package org.springframework.test.context.bean.override.mockito.typelevel; import org.springframework.test.context.bean.override.mockito.MockitoBean; @MockitoBean(types = Service11.class) -interface TestInterface11 { +interface MockTestInterface11 { } 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/typelevel/MockitoBeansByNameIntegrationTests.java similarity index 88% rename from spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/MockitoBeansByNameIntegrationTests.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/MockitoBeansByNameIntegrationTests.java index b27e2dd860af..272f767e4a98 100644 --- 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/typelevel/MockitoBeansByNameIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.bean.override.mockito.mockbeans; +package org.springframework.test.context.bean.override.mockito.typelevel; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -30,6 +30,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; +import static org.springframework.test.mockito.MockitoAssertions.assertIsMock; +import static org.springframework.test.mockito.MockitoAssertions.assertIsNotMock; /** * Integration tests for {@link MockitoBeans @MockitoBeans} and @@ -69,12 +71,18 @@ void configureMocks() { @Test void checkMocksAndStandardBean() { + assertIsMock(s1, "s1"); + assertIsMock(s2, "s2"); + assertIsMock(service3, "service3"); + assertIsNotMock(service4, "service4"); + 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 { 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/typelevel/MockitoBeansByTypeIntegrationTests.java similarity index 78% rename from spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/MockitoBeansByTypeIntegrationTests.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/MockitoBeansByTypeIntegrationTests.java index e58b2e888ce1..ccae3865b345 100644 --- 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/typelevel/MockitoBeansByTypeIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.bean.override.mockito.mockbeans; +package org.springframework.test.context.bean.override.mockito.typelevel; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; @@ -27,6 +27,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; +import static org.springframework.test.mockito.MockitoAssertions.assertIsMock; /** * Integration tests for {@link MockitoBeans @MockitoBeans} and @@ -42,7 +43,7 @@ @MockitoBean(types = {Service04.class, Service05.class}) @SharedMocks // Intentionally declared between local @MockitoBean declarations @MockitoBean(types = Service06.class) -class MockitoBeansByTypeIntegrationTests implements TestInterface01 { +class MockitoBeansByTypeIntegrationTests implements MockTestInterface01 { @Autowired Service01 service01; @@ -79,6 +80,14 @@ void configureMocks() { @Test void checkMocks() { + assertIsMock(service01, "service01"); + assertIsMock(service02, "service02"); + assertIsMock(service03, "service03"); + assertIsMock(service04, "service04"); + assertIsMock(service05, "service05"); + assertIsMock(service06, "service06"); + assertIsMock(service07, "service07"); + assertThat(service01.greeting()).isEqualTo("mock 01"); assertThat(service02.greeting()).isEqualTo("mock 02"); assertThat(service03.greeting()).isEqualTo("mock 03"); @@ -90,7 +99,7 @@ void checkMocks() { @MockitoBean(types = Service09.class) - class BaseTestCase implements TestInterface08 { + class BaseTestCase implements MockTestInterface08 { @Autowired Service08 service08; @@ -104,7 +113,7 @@ class BaseTestCase implements TestInterface08 { @Nested @MockitoBean(types = Service12.class) - class NestedTests extends BaseTestCase implements TestInterface11 { + class NestedTests extends BaseTestCase implements MockTestInterface11 { @Autowired Service11 service11; @@ -128,6 +137,20 @@ void configureMocks() { @Test void checkMocks() { + assertIsMock(service01, "service01"); + assertIsMock(service02, "service02"); + assertIsMock(service03, "service03"); + assertIsMock(service04, "service04"); + assertIsMock(service05, "service05"); + assertIsMock(service06, "service06"); + assertIsMock(service07, "service07"); + assertIsMock(service08, "service08"); + assertIsMock(service09, "service09"); + assertIsMock(service10, "service10"); + assertIsMock(service11, "service11"); + assertIsMock(service12, "service12"); + assertIsMock(service13, "service13"); + assertThat(service01.greeting()).isEqualTo("mock 01"); assertThat(service02.greeting()).isEqualTo("mock 02"); assertThat(service03.greeting()).isEqualTo("mock 03"); 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/typelevel/MockitoBeansTests.java similarity index 97% rename from spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/MockitoBeansTests.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/MockitoBeansTests.java index 3ef3e14ea2e2..28b4f87e8c72 100644 --- 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/typelevel/MockitoBeansTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.bean.override.mockito.mockbeans; +package org.springframework.test.context.bean.override.mockito.typelevel; import java.util.stream.Stream; diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/MockitoSpyBeansByNameIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/MockitoSpyBeansByNameIntegrationTests.java new file mode 100644 index 000000000000..b35c03aebd71 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/MockitoSpyBeansByNameIntegrationTests.java @@ -0,0 +1,127 @@ +/* + * 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.typelevel; + +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.MockitoSpyBean; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBeans; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.springframework.test.mockito.MockitoAssertions.assertIsNotMock; +import static org.springframework.test.mockito.MockitoAssertions.assertIsNotSpy; +import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy; + +/** + * Integration tests for {@link MockitoSpyBeans @MockitoSpyBeans} and + * {@link MockitoSpyBean @MockitoSpyBean} declared "by name" at the class level + * as a repeatable annotation. + * + * @author Sam Brannen + * @since 6.2.3 + * @see gh-34408 + * @see MockitoSpyBeansByTypeIntegrationTests + */ +@SpringJUnitConfig +@MockitoSpyBean(name = "s1", types = ExampleService.class) +@MockitoSpyBean(name = "s2", types = ExampleService.class) +class MockitoSpyBeansByNameIntegrationTests { + + @Autowired + ExampleService s1; + + @Autowired + ExampleService s2; + + @MockitoSpyBean(name = "s3") + ExampleService service3; + + @Autowired + @Qualifier("s4") + ExampleService service4; + + + @BeforeEach + void configureSpies() { + given(s1.greeting()).willReturn("spy 1"); + given(s2.greeting()).willReturn("spy 2"); + given(service3.greeting()).willReturn("spy 3"); + } + + @Test + void checkSpiesAndStandardBean() { + assertIsSpy(s1, "s1"); + assertIsSpy(s2, "s2"); + assertIsSpy(service3, "service3"); + assertIsNotMock(service4, "service4"); + assertIsNotSpy(service4, "service4"); + + assertThat(s1.greeting()).isEqualTo("spy 1"); + assertThat(s2.greeting()).isEqualTo("spy 2"); + assertThat(service3.greeting()).isEqualTo("spy 3"); + assertThat(service4.greeting()).isEqualTo("prod 4"); + } + + + @Configuration + static class Config { + + @Bean + ExampleService s1() { + return new ExampleService() { + @Override + public String greeting() { + return "prod 1"; + } + }; + } + + @Bean + ExampleService s2() { + return new ExampleService() { + @Override + public String greeting() { + return "prod 2"; + } + }; + } + + @Bean + ExampleService s3() { + return new ExampleService() { + @Override + public String greeting() { + return "prod 3"; + } + }; + } + + @Bean + ExampleService s4() { + return () -> "prod 4"; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/MockitoSpyBeansByTypeIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/MockitoSpyBeansByTypeIntegrationTests.java new file mode 100644 index 000000000000..5454976fa2e2 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/MockitoSpyBeansByTypeIntegrationTests.java @@ -0,0 +1,307 @@ +/* + * 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.typelevel; + +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.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBeans; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy; + +/** + * Integration tests for {@link MockitoSpyBeans @MockitoSpyBeans} and + * {@link MockitoSpyBean @MockitoSpyBean} declared "by type" at the class level, + * as a repeatable annotation, and via a custom composed annotation. + * + * @author Sam Brannen + * @since 6.2.3 + * @see gh-34408 + * @see MockitoSpyBeansByNameIntegrationTests + */ +@SpringJUnitConfig +@MockitoSpyBean(types = {Service04.class, Service05.class}) +@SharedSpies // Intentionally declared between local @MockitoSpyBean declarations +@MockitoSpyBean(types = Service06.class) +class MockitoSpyBeansByTypeIntegrationTests implements SpyTestInterface01 { + + @Autowired + Service01 service01; + + @Autowired + Service02 service02; + + @Autowired + Service03 service03; + + @Autowired + Service04 service04; + + @Autowired + Service05 service05; + + @Autowired + Service06 service06; + + @MockitoSpyBean + Service07 service07; + + + @BeforeEach + void configureSpies() { + given(service01.greeting()).willReturn("spy 01"); + given(service02.greeting()).willReturn("spy 02"); + given(service03.greeting()).willReturn("spy 03"); + given(service04.greeting()).willReturn("spy 04"); + given(service05.greeting()).willReturn("spy 05"); + given(service06.greeting()).willReturn("spy 06"); + given(service07.greeting()).willReturn("spy 07"); + } + + @Test + void checkSpies() { + assertIsSpy(service01, "service01"); + assertIsSpy(service02, "service02"); + assertIsSpy(service03, "service03"); + assertIsSpy(service04, "service04"); + assertIsSpy(service05, "service05"); + assertIsSpy(service06, "service06"); + assertIsSpy(service07, "service07"); + + assertThat(service01.greeting()).isEqualTo("spy 01"); + assertThat(service02.greeting()).isEqualTo("spy 02"); + assertThat(service03.greeting()).isEqualTo("spy 03"); + assertThat(service04.greeting()).isEqualTo("spy 04"); + assertThat(service05.greeting()).isEqualTo("spy 05"); + assertThat(service06.greeting()).isEqualTo("spy 06"); + assertThat(service07.greeting()).isEqualTo("spy 07"); + } + + + @MockitoSpyBean(types = Service09.class) + class BaseTestCase implements SpyTestInterface08 { + + @Autowired + Service08 service08; + + @Autowired + Service09 service09; + + @MockitoSpyBean + Service10 service10; + } + + @Nested + @MockitoSpyBean(types = Service12.class) + class NestedTests extends BaseTestCase implements SpyTestInterface11 { + + @Autowired + Service11 service11; + + @Autowired + Service12 service12; + + @MockitoSpyBean + Service13 service13; + + + @BeforeEach + void configureSpies() { + given(service08.greeting()).willReturn("spy 08"); + given(service09.greeting()).willReturn("spy 09"); + given(service10.greeting()).willReturn("spy 10"); + given(service11.greeting()).willReturn("spy 11"); + given(service12.greeting()).willReturn("spy 12"); + given(service13.greeting()).willReturn("spy 13"); + } + + @Test + void checkSpies() { + assertIsSpy(service01, "service01"); + assertIsSpy(service02, "service02"); + assertIsSpy(service03, "service03"); + assertIsSpy(service04, "service04"); + assertIsSpy(service05, "service05"); + assertIsSpy(service06, "service06"); + assertIsSpy(service07, "service07"); + assertIsSpy(service08, "service08"); + assertIsSpy(service09, "service09"); + assertIsSpy(service10, "service10"); + assertIsSpy(service11, "service11"); + assertIsSpy(service12, "service12"); + assertIsSpy(service13, "service13"); + + assertThat(service01.greeting()).isEqualTo("spy 01"); + assertThat(service02.greeting()).isEqualTo("spy 02"); + assertThat(service03.greeting()).isEqualTo("spy 03"); + assertThat(service04.greeting()).isEqualTo("spy 04"); + assertThat(service05.greeting()).isEqualTo("spy 05"); + assertThat(service06.greeting()).isEqualTo("spy 06"); + assertThat(service07.greeting()).isEqualTo("spy 07"); + assertThat(service08.greeting()).isEqualTo("spy 08"); + assertThat(service09.greeting()).isEqualTo("spy 09"); + assertThat(service10.greeting()).isEqualTo("spy 10"); + assertThat(service11.greeting()).isEqualTo("spy 11"); + assertThat(service12.greeting()).isEqualTo("spy 12"); + assertThat(service13.greeting()).isEqualTo("spy 13"); + } + } + + + @Configuration + static class Config { + + @Bean + Service01 service01() { + return new Service01() { + @Override + public String greeting() { + return "prod 1"; + } + }; + } + + @Bean + Service02 service02() { + return new Service02() { + @Override + public String greeting() { + return "prod 2"; + } + }; + } + + @Bean + Service03 service03() { + return new Service03() { + @Override + public String greeting() { + return "prod 3"; + } + }; + } + + @Bean + Service04 service04() { + return new Service04() { + @Override + public String greeting() { + return "prod 4"; + } + }; + } + + @Bean + Service05 service05() { + return new Service05() { + @Override + public String greeting() { + return "prod 5"; + } + }; + } + + @Bean + Service06 service06() { + return new Service06() { + @Override + public String greeting() { + return "prod 6"; + } + }; + } + + @Bean + Service07 service07() { + return new Service07() { + @Override + public String greeting() { + return "prod 7"; + } + }; + } + + @Bean + Service08 service08() { + return new Service08() { + @Override + public String greeting() { + return "prod 8"; + } + }; + } + + @Bean + Service09 service09() { + return new Service09() { + @Override + public String greeting() { + return "prod 9"; + } + }; + } + + @Bean + Service10 service10() { + return new Service10() { + @Override + public String greeting() { + return "prod 10"; + } + }; + } + + @Bean + Service11 service11() { + return new Service11() { + @Override + public String greeting() { + return "prod 11"; + } + }; + } + + @Bean + Service12 service12() { + return new Service12() { + @Override + public String greeting() { + return "prod 12"; + } + }; + } + + @Bean + Service13 service13() { + return new Service13() { + @Override + public String greeting() { + return "prod 13"; + } + }; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/MockitoSpyBeansTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/MockitoSpyBeansTests.java new file mode 100644 index 000000000000..af0985e456e7 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/MockitoSpyBeansTests.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.typelevel; + +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.MockitoSpyBean; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBeans; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MockitoSpyBeans @MockitoSpyBeans}: {@link MockitoSpyBean @MockitoSpyBean} + * declared at the class level, as a repeatable annotation, and via a custom composed + * annotation. + * + * @author Sam Brannen + * @since 6.2.3 + * @see gh-34408 + */ +class MockitoSpyBeansTests { + + @Test + void registrationOrderForTopLevelClass() { + Stream> mockedServices = getRegisteredMockTypes(MockitoSpyBeansByTypeIntegrationTests.class); + assertThat(mockedServices).containsExactly( + Service01.class, Service02.class, Service03.class, Service04.class, + Service05.class, Service06.class, Service07.class); + } + + @Test + void registrationOrderForNestedClass() { + Stream> mockedServices = getRegisteredMockTypes(MockitoSpyBeansByTypeIntegrationTests.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/typelevel/Service.java similarity index 90% rename from spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/Service.java index 187ffeb6a833..e0092704f82b 100644 --- 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/typelevel/Service.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.bean.override.mockito.mockbeans; +package org.springframework.test.context.bean.override.mockito.typelevel; 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/typelevel/Service01.java similarity index 89% rename from spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service01.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/Service01.java index a2110bf7fb5a..e99bb762659a 100644 --- 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/typelevel/Service01.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.bean.override.mockito.mockbeans; +package org.springframework.test.context.bean.override.mockito.typelevel; 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/typelevel/Service02.java similarity index 89% rename from spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service02.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/Service02.java index c2b62a558eec..78de4a2efb0e 100644 --- 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/typelevel/Service02.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.bean.override.mockito.mockbeans; +package org.springframework.test.context.bean.override.mockito.typelevel; 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/typelevel/Service03.java similarity index 89% rename from spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service03.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/Service03.java index 31fe690f5944..564a48ceac1d 100644 --- 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/typelevel/Service03.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.bean.override.mockito.mockbeans; +package org.springframework.test.context.bean.override.mockito.typelevel; 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/typelevel/Service04.java similarity index 89% rename from spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service04.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/Service04.java index d32ba233d478..25c70404d1fc 100644 --- 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/typelevel/Service04.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.bean.override.mockito.mockbeans; +package org.springframework.test.context.bean.override.mockito.typelevel; 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/typelevel/Service05.java similarity index 89% rename from spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service05.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/Service05.java index 8c738aa36dde..d8da96f5466b 100644 --- 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/typelevel/Service05.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.bean.override.mockito.mockbeans; +package org.springframework.test.context.bean.override.mockito.typelevel; 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/typelevel/Service06.java similarity index 89% rename from spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service06.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/Service06.java index bceab3996501..00adf7c28e5a 100644 --- 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/typelevel/Service06.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.bean.override.mockito.mockbeans; +package org.springframework.test.context.bean.override.mockito.typelevel; 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/typelevel/Service07.java similarity index 89% rename from spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service07.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/Service07.java index 35e82c9fdad8..d3715f93748d 100644 --- 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/typelevel/Service07.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.bean.override.mockito.mockbeans; +package org.springframework.test.context.bean.override.mockito.typelevel; 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/typelevel/Service08.java similarity index 89% rename from spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service08.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/Service08.java index d9630716cf73..af5f07ced931 100644 --- 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/typelevel/Service08.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.bean.override.mockito.mockbeans; +package org.springframework.test.context.bean.override.mockito.typelevel; 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/typelevel/Service09.java similarity index 89% rename from spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service09.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/Service09.java index cb5e92242eee..363f591882e7 100644 --- 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/typelevel/Service09.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.bean.override.mockito.mockbeans; +package org.springframework.test.context.bean.override.mockito.typelevel; 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/typelevel/Service10.java similarity index 89% rename from spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service10.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/Service10.java index 50231a138491..1fcaaff6eb7c 100644 --- 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/typelevel/Service10.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.bean.override.mockito.mockbeans; +package org.springframework.test.context.bean.override.mockito.typelevel; 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/typelevel/Service11.java similarity index 89% rename from spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service11.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/Service11.java index 3b9203d5ee88..1c8b7cb33e9c 100644 --- 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/typelevel/Service11.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.bean.override.mockito.mockbeans; +package org.springframework.test.context.bean.override.mockito.typelevel; 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/typelevel/Service12.java similarity index 89% rename from spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service12.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/Service12.java index 097d5ade6c47..386b41294c93 100644 --- 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/typelevel/Service12.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.bean.override.mockito.mockbeans; +package org.springframework.test.context.bean.override.mockito.typelevel; 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/typelevel/Service13.java similarity index 89% rename from spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service13.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/Service13.java index 233e52be6f25..5ac3a2828e47 100644 --- 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/typelevel/Service13.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.bean.override.mockito.mockbeans; +package org.springframework.test.context.bean.override.mockito.typelevel; 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/typelevel/SharedMocks.java similarity index 93% rename from spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/SharedMocks.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/SharedMocks.java index e44017dc6af1..39c9056613df 100644 --- 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/typelevel/SharedMocks.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.bean.override.mockito.mockbeans; +package org.springframework.test.context.bean.override.mockito.typelevel; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/SharedSpies.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/SharedSpies.java new file mode 100644 index 000000000000..f3cb10d7ba87 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/SharedSpies.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.typelevel; + +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.MockitoSpyBean; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@MockitoSpyBean(types = Service02.class) +@MockitoSpyBean(types = Service03.class) +@interface SharedSpies { +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/SpyTestInterface01.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/SpyTestInterface01.java new file mode 100644 index 000000000000..9f8c24193964 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/SpyTestInterface01.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.typelevel; + +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; + +@MockitoSpyBean(types = Service01.class) +interface SpyTestInterface01 { +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/SpyTestInterface08.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/SpyTestInterface08.java new file mode 100644 index 000000000000..37e74e64f05b --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/SpyTestInterface08.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.typelevel; + +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; + +@MockitoSpyBean(types = Service08.class) +interface SpyTestInterface08 { +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/SpyTestInterface11.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/SpyTestInterface11.java new file mode 100644 index 000000000000..ed5bf93472b1 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/SpyTestInterface11.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.typelevel; + +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; + +@MockitoSpyBean(types = Service11.class) +interface SpyTestInterface11 { +} diff --git a/spring-test/src/test/java/org/springframework/test/mockito/MockitoAssertions.java b/spring-test/src/test/java/org/springframework/test/mockito/MockitoAssertions.java index b1d36e5dd306..c4bbfb814842 100644 --- a/spring-test/src/test/java/org/springframework/test/mockito/MockitoAssertions.java +++ b/spring-test/src/test/java/org/springframework/test/mockito/MockitoAssertions.java @@ -31,10 +31,12 @@ public abstract class MockitoAssertions { public static void assertIsMock(Object obj) { assertThat(isMock(obj)).as("is a Mockito mock").isTrue(); + assertIsNotSpy(obj); } public static void assertIsMock(Object obj, String message) { assertThat(isMock(obj)).as("%s is a Mockito mock", message).isTrue(); + assertIsNotSpy(obj, message); } public static void assertIsNotMock(Object obj) {