From 43f13e4d30db47495084235cca99ffb445ffedbf Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 15 Jan 2025 10:49:27 +0100 Subject: [PATCH 1/2] =?UTF-8?q?Cross=20reference=20@=E2=81=A0NestedTestCon?= =?UTF-8?q?figuration=20for=20Bean=20Overrides?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test/context/NestedTestConfiguration.java | 5 ++++- .../test/context/bean/override/convention/TestBean.java | 6 +++++- .../test/context/bean/override/mockito/MockitoBean.java | 4 ++++ .../test/context/bean/override/mockito/MockitoSpyBean.java | 6 +++++- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/context/NestedTestConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/NestedTestConfiguration.java index d7102667e0c9..9b716cc7f4ef 100644 --- a/spring-test/src/main/java/org/springframework/test/context/NestedTestConfiguration.java +++ b/spring-test/src/main/java/org/springframework/test/context/NestedTestConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -82,6 +82,9 @@ *
  • {@link ActiveProfiles @ActiveProfiles}
  • *
  • {@link TestPropertySource @TestPropertySource}
  • *
  • {@link DynamicPropertySource @DynamicPropertySource}
  • + *
  • {@link org.springframework.test.context.bean.override.convention.TestBean @TestBean}
  • + *
  • {@link org.springframework.test.context.bean.override.mockito.MockitoBean @MockitoBean}
  • + *
  • {@link org.springframework.test.context.bean.override.mockito.MockitoSpyBean @MockitoSpyBean}
  • *
  • {@link org.springframework.test.annotation.DirtiesContext @DirtiesContext}
  • *
  • {@link org.springframework.transaction.annotation.Transactional @Transactional}
  • *
  • {@link org.springframework.test.annotation.Rollback @Rollback}
  • diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java index f6a63ed4574f..9393a17ed0cb 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -110,6 +110,10 @@ * {@code protected}, package-private (default visibility), or {@code private} * depending on the needs or coding practices of the project. * + *

    {@code @TestBean} fields will be inherited from an enclosing test class by default. See + * {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration} + * for details. + * * @author Simon Baslé * @author Stephane Nicoll * @author Sam Brannen diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java index f01d0fc10b53..640a75bb22e9 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java @@ -63,6 +63,10 @@ * (default visibility), or {@code private} depending on the needs or coding * practices of the project. * + *

    {@code @MockitoBean} fields will be inherited from an enclosing test class by default. + * See {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration} + * for details. + * * @author Simon Baslé * @author Sam Brannen * @since 6.2 diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBean.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBean.java index 6320a54845b1..d10674b75331 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBean.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -56,6 +56,10 @@ * (default visibility), or {@code private} depending on the needs or coding * practices of the project. * + *

    {@code @MockitoSpyBean} fields will be inherited from an enclosing test class by default. + * See {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration} + * for details. + * * @author Simon Baslé * @author Sam Brannen * @since 6.2 From 405eb7c1474d2df5c55338e6bc11be50063f332b Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 3 Jan 2025 12:09:04 +0200 Subject: [PATCH 2/2] =?UTF-8?q?Support=20@=E2=81=A0MockitoBean=20at=20the?= =?UTF-8?q?=20type=20level=20on=20test=20classes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prior to this commit, @⁠MockitoBean could only be declared on fields within test classes, which prevented developers from being able to easily reuse mock configuration across a test suite. With this commit, @⁠MockitoBean is now supported at the type level on test classes, their superclasses, and interfaces implemented by those classes. @⁠MockitoBean is also supported on enclosing classes for @⁠Nested test classes, their superclasses, and interfaces implemented by those classes, while honoring @⁠NestedTestConfiguration semantics. In addition, @⁠MockitoBean: - has a new `types` attribute that can be used to declare the type or types to mock when @⁠MockitoBean is declared at the type level - can be declared as a repeatable annotation at the type level - can be declared as a meta-annotation on a custom composed annotation which can be reused across a test suite (see the @⁠SharedMocks example in the reference manual) To support these new features, this commit also includes the following changes. - The `field` property in BeanOverrideHandler is now @⁠Nullable. - BeanOverrideProcessor has a new `default` createHandlers() method which is invoked when a @⁠BeanOverride annotation is found at the type level. - MockitoBeanOverrideProcessor implements the new createHandlers() method. - The internal findHandlers() method in BeanOverrideHandler has been completely overhauled. - The @⁠MockitoBean and @⁠MockitoSpyBean section of the reference manual has been completely overhauled. Closes gh-33925 --- .../annotation-mockitobean.adoc | 142 ++++++++++++--- .../context/bean/override/BeanOverride.java | 12 +- .../BeanOverrideBeanFactoryPostProcessor.java | 70 ++++---- .../BeanOverrideContextCustomizerFactory.java | 6 +- .../bean/override/BeanOverrideHandler.java | 132 ++++++++++---- .../bean/override/BeanOverrideProcessor.java | 38 +++- .../bean/override/BeanOverrideRegistry.java | 4 +- .../AbstractMockitoBeanOverrideHandler.java | 2 +- .../bean/override/mockito/MockitoBean.java | 75 ++++++-- .../mockito/MockitoBeanOverrideHandler.java | 8 +- .../mockito/MockitoBeanOverrideProcessor.java | 26 +++ .../bean/override/mockito/MockitoBeans.java | 41 +++++ .../bean/override/BeanOverrideTestUtils.java | 4 + .../MockitoBeanOverrideHandlerTests.java | 101 ++++++++++- .../MockitoBeanOverrideProcessorTests.java | 164 +++++++++++++++--- .../MockitoBeansByNameIntegrationTests.java | 102 +++++++++++ .../MockitoBeansByTypeIntegrationTests.java | 147 ++++++++++++++++ .../mockito/mockbeans/MockitoBeansTests.java | 68 ++++++++ .../override/mockito/mockbeans/Service.java | 21 +++ .../override/mockito/mockbeans/Service01.java | 20 +++ .../override/mockito/mockbeans/Service02.java | 20 +++ .../override/mockito/mockbeans/Service03.java | 20 +++ .../override/mockito/mockbeans/Service04.java | 20 +++ .../override/mockito/mockbeans/Service05.java | 20 +++ .../override/mockito/mockbeans/Service06.java | 20 +++ .../override/mockito/mockbeans/Service07.java | 20 +++ .../override/mockito/mockbeans/Service08.java | 20 +++ .../override/mockito/mockbeans/Service09.java | 20 +++ .../override/mockito/mockbeans/Service10.java | 20 +++ .../override/mockito/mockbeans/Service11.java | 20 +++ .../override/mockito/mockbeans/Service12.java | 20 +++ .../override/mockito/mockbeans/Service13.java | 20 +++ .../mockito/mockbeans/SharedMocks.java | 31 ++++ .../mockito/mockbeans/TestInterface01.java | 23 +++ .../mockito/mockbeans/TestInterface08.java | 23 +++ .../mockito/mockbeans/TestInterface11.java | 23 +++ 36 files changed, 1379 insertions(+), 144 deletions(-) create mode 100644 spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeans.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/MockitoBeansByNameIntegrationTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/MockitoBeansByTypeIntegrationTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/MockitoBeansTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service01.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service02.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service03.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service04.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service05.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service06.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service07.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service08.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service09.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service10.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service11.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service12.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service13.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/SharedMocks.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/TestInterface01.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/TestInterface08.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/TestInterface11.java diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc index d46508f3d9a4..6bb17af198d4 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc @@ -11,29 +11,21 @@ If multiple candidates match, `@Qualifier` can be provided to narrow the candida override. Alternatively, a candidate whose bean name matches the name of the field will match. -When using `@MockitoBean`, a new bean will be created if a corresponding bean does not -exist. However, if you would like for the test to fail when a corresponding bean does not -exist, you can set the `enforceOverride` attribute to `true` – for example, -`@MockitoBean(enforceOverride = true)`. - -To use a by-name override rather than a by-type override, specify the `name` attribute -of the annotation. - [WARNING] ==== Qualifiers, including the name of the field, are used to determine if a separate `ApplicationContext` needs to be created. If you are using this feature to mock or spy -the same bean in several tests, make sure to name the field consistently to avoid +the same bean in several test classes, make sure to name the field consistently to avoid creating unnecessary contexts. ==== -Each annotation also defines Mockito-specific attributes to fine-tune the mocking details. +Each annotation also defines Mockito-specific attributes to fine-tune the mocking behavior. -By default, the `@MockitoBean` annotation uses the `REPLACE_OR_CREATE` +The `@MockitoBean` annotation uses the `REPLACE_OR_CREATE` xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy for test bean overriding]. -If no existing bean matches, a new bean is created on the fly. As mentioned previously, -you can switch to the `REPLACE` strategy by setting the `enforceOverride` attribute to -`true`. +If no existing bean matches, a new bean is created on the fly. However, you can switch to +the `REPLACE` strategy by setting the `enforceOverride` attribute to `true`. See the +following section for an example. The `@MockitoSpyBean` annotation uses the `WRAP` xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy], @@ -61,6 +53,17 @@ Such fields can therefore be `public`, `protected`, package-private (default vis or `private` depending on the needs or coding practices of the project. ==== +[[spring-testing-annotation-beanoverriding-mockitobean-examples]] +== `@MockitoBean` Examples + +When using `@MockitoBean`, a new bean will be created if a corresponding bean does not +exist. However, if you would like for the test to fail when a corresponding bean does not +exist, you can set the `enforceOverride` attribute to `true` – for example, +`@MockitoBean(enforceOverride = true)`. + +To use a by-name override rather than a by-type override, specify the `name` (or `value`) +attribute of the annotation. + The following example shows how to use the default behavior of the `@MockitoBean` annotation: [tabs] @@ -69,11 +72,13 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- - class OverrideBeanTests { + @SpringJUnitConfig(TestConfig.class) + class BeanOverrideTests { + @MockitoBean // <1> CustomService customService; - // test case body... + // tests... } ---- <1> Replace the bean with type `CustomService` with a Mockito `mock`. @@ -82,8 +87,8 @@ Java:: In the example above, we are creating a mock for `CustomService`. If more than one bean of that type exists, the bean named `customService` is considered. Otherwise, the test will fail, and you will need to provide a qualifier of some sort to identify which of the -`CustomService` beans you want to override. If no such bean exists, a bean definition -will be created with an auto-generated bean name. +`CustomService` beans you want to override. If no such bean exists, a bean will be +created with an auto-generated bean name. The following example uses a by-name lookup, rather than a by-type lookup: @@ -93,20 +98,43 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- - class OverrideBeanTests { + @SpringJUnitConfig(TestConfig.class) + class BeanOverrideTests { + @MockitoBean("service") // <1> CustomService customService; - // test case body... + // tests... } ---- <1> Replace the bean named `service` with a Mockito `mock`. ====== -If no bean definition named `service` exists, one is created. +If no bean named `service` exists, one is created. + +`@MockitoBean` can also be used at the type level: + +- on a test class or any superclass or implemented interface in the type hierarchy above + the test class +- on an enclosing class for a `@Nested` test class or on any class or interface in the + type hierarchy or enclosing class hierarchy above the `@Nested` test class -The following example shows how to use the default behavior of the `@MockitoSpyBean` annotation: +When `@MockitoBean` is declared at the type level, the type of bean (or beans) to mock +must be supplied via the `types` attribute – for example, +`@MockitoBean(types = {OrderService.class, UserService.class})`. If multiple candidates +exist in the application context, you can explicitly specify a bean name to mock by +setting the `name` attribute. Note, however, that the `types` attribute must contain a +single type if an explicit bean `name` is configured – for example, +`@MockitoBean(name = "ps1", types = PrintingService.class)`. + +To support reuse of mock configuration, `@MockitoBean` may be used as a meta-annotation +to create custom _composed annotations_ — for example, to define common mock +configuration in a single annotation that can be reused across a test suite. +`@MockitoBean` can also be used as a repeatable annotation at the type level — for +example, to mock several beans by name. + +The following `@SharedMocks` annotation registers two mocks by-type and one mock by-name. [tabs] ====== @@ -114,11 +142,70 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- - class OverrideBeanTests { + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @MockitoBean(types = {OrderService.class, UserService.class}) // <1> + @MockitoBean(name = "ps1", types = PrintingService.class) // <2> + public @interface SharedMocks { + } +---- +<1> Register `OrderService` and `UserService` mocks by-type. +<2> Register `PrintingService` mock by-name. +====== + +The following demonstrates how `@SharedMocks` can be used on a test class. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig(TestConfig.class) + @SharedMocks // <1> + class BeanOverrideTests { + + @Autowired OrderService orderService; // <2> + + @Autowired UserService userService; // <2> + + @Autowired PrintingService ps1; // <2> + + // Inject other components that rely on the mocks. + + @Test + void testThatDependsOnMocks() { + // ... + } + } +---- +<1> Register common mocks via the custom `@SharedMocks` annotation. +<2> Optionally inject mocks to _stub_ or _verify_ them. +====== + +TIP: The mocks can also be injected into `@Configuration` classes or other test-related +components in the `ApplicationContext` in order to configure them with Mockito's stubbing +APIs. + +[[spring-testing-annotation-beanoverriding-mockitospybean-examples]] +== `@MockitoSpyBean` Examples + +The following example shows how to use the default behavior of the `@MockitoSpyBean` +annotation: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig(TestConfig.class) + class BeanOverrideTests { + @MockitoSpyBean // <1> CustomService customService; - // test case body... + // tests... } ---- <1> Wrap the bean with type `CustomService` with a Mockito `spy`. @@ -137,12 +224,13 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- - class OverrideBeanTests { + @SpringJUnitConfig(TestConfig.class) + class BeanOverrideTests { + @MockitoSpyBean("service") // <1> CustomService customService; - // test case body... - + // tests... } ---- <1> Wrap the bean named `service` with a Mockito `spy`. diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverride.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverride.java index 910eb5019562..9efd7948c547 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverride.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverride.java @@ -25,15 +25,19 @@ import org.springframework.aot.hint.annotation.Reflective; /** - * Mark a composed annotation as eligible for Bean Override processing. + * Mark a composed annotation as eligible for Bean Override processing. * *

    Specifying this annotation registers the configured {@link BeanOverrideProcessor} * which must be capable of handling the composed annotation and its attributes. * - *

    Since the composed annotation should only be applied to non-static fields, it is - * expected that it is meta-annotated with {@link Target @Target(ElementType.FIELD)}. + *

    Since the composed annotation will typically only be applied to non-static + * fields, it is expected that the composed annotation is meta-annotated with + * {@link Target @Target(ElementType.FIELD)}. However, certain bean override + * annotations may be declared with an additional {@code ElementType.TYPE} target + * for use at the type level, as is the case for {@code @MockitoBean} which can + * be declared on a field, test class, or test interface. * - *

    For concrete examples, see + *

    For concrete examples of such composed annotations, see * {@link org.springframework.test.context.bean.override.convention.TestBean @TestBean}, * {@link org.springframework.test.context.bean.override.mockito.MockitoBean @MockitoBean}, and * {@link org.springframework.test.context.bean.override.mockito.MockitoSpyBean @MockitoSpyBean}. diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java index 4d1f0f0d8532..c9224bc70b3a 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -104,11 +104,10 @@ private void registerBeanOverride(ConfigurableListableBeanFactory beanFactory, B Set generatedBeanNames) { String beanName = handler.getBeanName(); - Field field = handler.getField(); - Assert.state(!BeanFactoryUtils.isFactoryDereference(beanName),() -> """ - Unable to override bean '%s' for field '%s.%s': a FactoryBean cannot be overridden. \ - To override the bean created by the FactoryBean, remove the '&' prefix.""".formatted( - beanName, field.getDeclaringClass().getSimpleName(), field.getName())); + Assert.state(!BeanFactoryUtils.isFactoryDereference(beanName), () -> """ + Unable to override bean '%s'%s: a FactoryBean cannot be overridden. \ + To override the bean created by the FactoryBean, remove the '&' prefix.""" + .formatted(beanName, forField(handler.getField()))); switch (handler.getStrategy()) { case REPLACE -> replaceOrCreateBean(beanFactory, handler, generatedBeanNames, true); @@ -134,7 +133,6 @@ private void replaceOrCreateBean(ConfigurableListableBeanFactory beanFactory, Be // 4) Create bean by-name, with a provided name String beanName = handler.getBeanName(); - Field field = handler.getField(); BeanDefinition existingBeanDefinition = null; if (beanName == null) { beanName = getBeanNameForType(beanFactory, handler, requireExistingBean); @@ -169,11 +167,10 @@ private void replaceOrCreateBean(ConfigurableListableBeanFactory beanFactory, Be existingBeanDefinition = beanFactory.getBeanDefinition(beanName); } else if (requireExistingBean) { - throw new IllegalStateException(""" - Unable to replace bean: there is no bean with name '%s' and type %s \ - (as required by field '%s.%s').""" - .formatted(beanName, handler.getBeanType(), - field.getDeclaringClass().getSimpleName(), field.getName())); + Field field = handler.getField(); + throw new IllegalStateException( + "Unable to replace bean: there is no bean with name '%s' and type %s%s." + .formatted(beanName, handler.getBeanType(), requiredByField(field))); } // 4) We are creating a bean by-name with the provided beanName. } @@ -264,13 +261,11 @@ private void wrapBean(ConfigurableListableBeanFactory beanFactory, BeanOverrideH else { String message = "Unable to select a bean to wrap: "; if (candidateCount == 0) { - message += "there are no beans of type %s (as required by field '%s.%s')." - .formatted(beanType, field.getDeclaringClass().getSimpleName(), field.getName()); + message += "there are no beans of type %s%s.".formatted(beanType, requiredByField(field)); } else { - message += "found %d beans of type %s (as required by field '%s.%s'): %s" - .formatted(candidateCount, beanType, field.getDeclaringClass().getSimpleName(), - field.getName(), candidateNames); + message += "found %d beans of type %s%s: %s" + .formatted(candidateCount, beanType, requiredByField(field), candidateNames); } throw new IllegalStateException(message); } @@ -281,11 +276,9 @@ private void wrapBean(ConfigurableListableBeanFactory beanFactory, BeanOverrideH // We are wrapping an existing bean by-name. Set candidates = getExistingBeanNamesByType(beanFactory, handler, false); if (!candidates.contains(beanName)) { - throw new IllegalStateException(""" - Unable to wrap bean: there is no bean with name '%s' and type %s \ - (as required by field '%s.%s').""" - .formatted(beanName, beanType, field.getDeclaringClass().getSimpleName(), - field.getName())); + throw new IllegalStateException( + "Unable to wrap bean: there is no bean with name '%s' and type %s%s." + .formatted(beanName, beanType, requiredByField(field))); } } @@ -308,8 +301,8 @@ private String getBeanNameForType(ConfigurableListableBeanFactory beanFactory, B else if (candidateCount == 0) { if (requireExistingBean) { throw new IllegalStateException( - "Unable to override bean: there are no beans of type %s (as required by field '%s.%s')." - .formatted(beanType, field.getDeclaringClass().getSimpleName(), field.getName())); + "Unable to override bean: there are no beans of type %s%s." + .formatted(beanType, requiredByField(field))); } return null; } @@ -320,14 +313,14 @@ else if (candidateCount == 0) { } throw new IllegalStateException( - "Unable to select a bean to override: found %d beans of type %s (as required by field '%s.%s'): %s" - .formatted(candidateCount, beanType, field.getDeclaringClass().getSimpleName(), - field.getName(), candidateNames)); + "Unable to select a bean to override: found %d beans of type %s%s: %s" + .formatted(candidateCount, beanType, requiredByField(field), candidateNames)); } private Set getExistingBeanNamesByType(ConfigurableListableBeanFactory beanFactory, BeanOverrideHandler handler, boolean checkAutowiredCandidate) { + Field field = handler.getField(); ResolvableType resolvableType = handler.getBeanType(); Class type = resolvableType.toClass(); @@ -345,16 +338,16 @@ private Set getExistingBeanNamesByType(ConfigurableListableBeanFactory b } // Filter out non-matching autowire candidates. - if (checkAutowiredCandidate) { - DependencyDescriptor descriptor = new DependencyDescriptor(handler.getField(), true); + if (field != null && checkAutowiredCandidate) { + DependencyDescriptor descriptor = new DependencyDescriptor(field, true); beanNames.removeIf(beanName -> !beanFactory.isAutowireCandidate(beanName, descriptor)); } // Filter out scoped proxy targets. beanNames.removeIf(ScopedProxyUtils::isScopedTarget); // In case of multiple matches, fall back on the field's name as a last resort. - if (beanNames.size() > 1) { - String fieldName = handler.getField().getName(); + if (field != null && beanNames.size() > 1) { + String fieldName = field.getName(); if (beanNames.contains(fieldName)) { return Set.of(fieldName); } @@ -452,4 +445,19 @@ private static void destroySingleton(ConfigurableListableBeanFactory beanFactory dlbf.destroySingleton(beanName); } + private static String forField(@Nullable Field field) { + if (field == null) { + return ""; + } + return " for field '%s.%s'".formatted(field.getDeclaringClass().getSimpleName(), field.getName()); + } + + private static String requiredByField(@Nullable Field field) { + if (field == null) { + return ""; + } + return " (as required by field '%s.%s')".formatted( + field.getDeclaringClass().getSimpleName(), field.getName()); + } + } diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java index 515127174a0a..dfa9c9589eef 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java @@ -23,7 +23,6 @@ import org.springframework.lang.Nullable; import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextCustomizerFactory; -import org.springframework.test.context.TestContextAnnotationUtils; import org.springframework.util.Assert; /** @@ -52,10 +51,7 @@ public BeanOverrideContextCustomizer createContextCustomizer(Class testClass, } private void findBeanOverrideHandlers(Class testClass, Set handlers) { - if (TestContextAnnotationUtils.searchEnclosingClass(testClass)) { - findBeanOverrideHandlers(testClass.getEnclosingClass(), handlers); - } - BeanOverrideHandler.forTestClass(testClass).forEach(handler -> + BeanOverrideHandler.findAllHandlers(testClass).forEach(handler -> Assert.state(handlers.add(handler), () -> "Duplicate BeanOverrideHandler discovered in test class %s: %s" .formatted(testClass.getName(), handler))); diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java index 2545fabdcc7d..db89dd6e185b 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java @@ -17,15 +17,18 @@ package org.springframework.test.context.bean.override; import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; import java.lang.reflect.Modifier; +import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.HashSet; -import java.util.LinkedList; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiConsumer; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.config.BeanDefinition; @@ -35,6 +38,7 @@ import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.style.ToStringCreator; import org.springframework.lang.Nullable; +import org.springframework.test.context.TestContextAnnotationUtils; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; @@ -56,8 +60,8 @@ * *

    Concrete implementations of {@code BeanOverrideHandler} can store additional * metadata to use during override {@linkplain #createOverrideInstance instance - * creation} — for example, based on further processing of the annotation - * or the annotated field. + * creation} — for example, based on further processing of the annotation, + * the annotated field, or the annotated class. * *

    NOTE: Only singleton beans can be overridden. * Any attempt to override a non-singleton bean will result in an exception. @@ -69,6 +73,11 @@ */ public abstract class BeanOverrideHandler { + private static final Comparator> reversedMetaDistance = + Comparator.> comparingInt(MergedAnnotation::getDistance).reversed(); + + + @Nullable private final Field field; private final Set qualifierAnnotations; @@ -81,7 +90,7 @@ public abstract class BeanOverrideHandler { private final BeanOverrideStrategy strategy; - protected BeanOverrideHandler(Field field, ResolvableType beanType, @Nullable String beanName, + protected BeanOverrideHandler(@Nullable Field field, ResolvableType beanType, @Nullable String beanName, BeanOverrideStrategy strategy) { this.field = field; @@ -95,57 +104,116 @@ protected BeanOverrideHandler(Field field, ResolvableType beanType, @Nullable St * Process the given {@code testClass} and build the corresponding * {@code BeanOverrideHandler} list derived from {@link BeanOverride @BeanOverride} * fields in the test class and its type hierarchy. - *

    This method does not search the enclosing class hierarchy. + *

    This method does not search the enclosing class hierarchy and does not + * search for {@code @BeanOverride} declarations on classes or interfaces. * @param testClass the test class to process * @return a list of bean override handlers - * @see org.springframework.test.context.TestContextAnnotationUtils#searchEnclosingClass(Class) + * @see #findAllHandlers(Class) */ public static List forTestClass(Class testClass) { - List handlers = new LinkedList<>(); - findHandlers(testClass, testClass, handlers); + return findHandlers(testClass, true); + } + + /** + * Process the given {@code testClass} and build the corresponding + * {@code BeanOverrideHandler} list derived from {@link BeanOverride @BeanOverride} + * fields in the test class and in its type hierarchy as well as from + * {@code @BeanOverride} declarations on classes and interfaces. + *

    This method additionally searches for {@code @BeanOverride} declarations + * in the enclosing class hierarchy based on + * {@link TestContextAnnotationUtils#searchEnclosingClass(Class)} semantics. + * @param testClass the test class to process + * @return a list of bean override handlers + * @since 6.2.2 + */ + static List findAllHandlers(Class testClass) { + return findHandlers(testClass, false); + } + + private static List findHandlers(Class testClass, boolean localFieldsOnly) { + List handlers = new ArrayList<>(); + findHandlers(testClass, testClass, handlers, localFieldsOnly); return handlers; } /** - * Find handlers using tail recursion to ensure that "locally declared" - * bean overrides take precedence over inherited bean overrides. + * Find handlers using tail recursion to ensure that "locally declared" bean overrides + * take precedence over inherited bean overrides. + *

    Note: the search algorithm is effectively the inverse of the algorithm used in + * {@link org.springframework.test.context.TestContextAnnotationUtils#findAnnotationDescriptor(Class, Class)}, + * but with tail recursion the semantics should be the same. + * @param clazz the class in/on which to search + * @param testClass the original test class + * @param handlers the list of handlers found + * @param localFieldsOnly whether to search only on local fields within the type hierarchy * @since 6.2.2 */ - private static void findHandlers(Class clazz, Class testClass, List handlers) { - if (clazz == null || clazz == Object.class) { - return; + private static void findHandlers(Class clazz, Class testClass, List handlers, + boolean localFieldsOnly) { + + // 1) Search enclosing class hierarchy. + if (!localFieldsOnly && TestContextAnnotationUtils.searchEnclosingClass(clazz)) { + findHandlers(clazz.getEnclosingClass(), testClass, handlers, localFieldsOnly); } - // 1) Search type hierarchy. - findHandlers(clazz.getSuperclass(), testClass, handlers); + // 2) Search class hierarchy. + Class superclass = clazz.getSuperclass(); + if (superclass != null && superclass != Object.class) { + findHandlers(superclass, testClass, handlers, localFieldsOnly); + } + + if (!localFieldsOnly) { + // 3) Search interfaces. + for (Class ifc : clazz.getInterfaces()) { + findHandlers(ifc, testClass, handlers, localFieldsOnly); + } + + // 4) Process current class. + processClass(clazz, testClass, handlers); + } - // 2) Process fields in current class. + // 5) Process fields in current class. ReflectionUtils.doWithLocalFields(clazz, field -> processField(field, testClass, handlers)); } + private static void processClass(Class clazz, Class testClass, List handlers) { + processElement(clazz, testClass, (processor, composedAnnotation) -> + processor.createHandlers(composedAnnotation, testClass).forEach(handlers::add)); + } + private static void processField(Field field, Class testClass, List handlers) { AtomicBoolean overrideAnnotationFound = new AtomicBoolean(); - MergedAnnotations.from(field, DIRECT).stream(BeanOverride.class).forEach(mergedAnnotation -> { + processElement(field, testClass, (processor, composedAnnotation) -> { Assert.state(!Modifier.isStatic(field.getModifiers()), () -> "@BeanOverride field must not be static: " + field); - MergedAnnotation metaSource = mergedAnnotation.getMetaSource(); - Assert.state(metaSource != null, "@BeanOverride annotation must be meta-present"); - - BeanOverride beanOverride = mergedAnnotation.synthesize(); - BeanOverrideProcessor processor = BeanUtils.instantiateClass(beanOverride.value()); - Annotation composedAnnotation = metaSource.synthesize(); - Assert.state(overrideAnnotationFound.compareAndSet(false, true), () -> "Multiple @BeanOverride annotations found on field: " + field); - BeanOverrideHandler handler = processor.createHandler(composedAnnotation, testClass, field); - handlers.add(handler); + handlers.add(processor.createHandler(composedAnnotation, testClass, field)); }); } + private static void processElement(AnnotatedElement element, Class testClass, + BiConsumer consumer) { + + MergedAnnotations.from(element, DIRECT) + .stream(BeanOverride.class) + .sorted(reversedMetaDistance) + .forEach(mergedAnnotation -> { + MergedAnnotation metaSource = mergedAnnotation.getMetaSource(); + Assert.state(metaSource != null, "@BeanOverride annotation must be meta-present"); + + BeanOverride beanOverride = mergedAnnotation.synthesize(); + BeanOverrideProcessor processor = BeanUtils.instantiateClass(beanOverride.value()); + Annotation composedAnnotation = metaSource.synthesize(); + consumer.accept(processor, composedAnnotation); + }); + } + /** * Get the annotated {@link Field}. */ + @Nullable public final Field getField() { return this.field; } @@ -249,7 +317,10 @@ public boolean equals(Object other) { } // by-type lookup - return (Objects.equals(this.field.getName(), that.field.getName()) && + if (this.field == null) { + return (that.field == null); + } + return (that.field != null && this.field.getName().equals(that.field.getName()) && this.qualifierAnnotations.equals(that.qualifierAnnotations)); } @@ -257,7 +328,7 @@ public boolean equals(Object other) { public int hashCode() { int hash = Objects.hash(getClass(), this.beanType.getType(), this.beanName, this.strategy); return (this.beanName != null ? hash : hash + - Objects.hash(this.field.getName(), this.qualifierAnnotations)); + Objects.hash((this.field != null ? this.field.getName() : null), this.qualifierAnnotations)); } @Override @@ -271,7 +342,10 @@ public String toString() { } - private static Set getQualifierAnnotations(Field field) { + private static Set getQualifierAnnotations(@Nullable Field field) { + if (field == null) { + return Collections.emptySet(); + } Annotation[] candidates = field.getDeclaredAnnotations(); if (candidates.length == 0) { return Collections.emptySet(); diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideProcessor.java index 87a363adb1eb..2e1f69ec90f7 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideProcessor.java @@ -18,10 +18,13 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Field; +import java.util.Collections; +import java.util.List; /** - * Strategy interface for Bean Override processing, which creates a - * {@link BeanOverrideHandler} that drives how the target bean is overridden. + * Strategy interface for Bean Override processing, which creates + * {@link BeanOverrideHandler} instances that drive how target beans are + * overridden. * *

    At least one composed annotation that is meta-annotated with * {@link BeanOverride @BeanOverride} must be a companion of this processor and @@ -40,12 +43,41 @@ public interface BeanOverrideProcessor { /** * Create a {@link BeanOverrideHandler} for the given annotated field. + *

    This method will only be invoked when a {@link BeanOverride @BeanOverride} + * annotation is declared on a field — for example, if the supplied field + * is annotated with {@code @MockitoBean}. * @param overrideAnnotation the composed annotation that declares the - * {@link BeanOverride @BeanOverride} annotation which registers this processor + * {@code @BeanOverride} annotation which registers this processor * @param testClass the test class to process * @param field the annotated field * @return the {@code BeanOverrideHandler} that should handle the given field + * @see #createHandlers(Annotation, Class) */ BeanOverrideHandler createHandler(Annotation overrideAnnotation, Class testClass, Field field); + /** + * Create a list of {@link BeanOverrideHandler} instances for the given override + * annotation and test class. + *

    This method will only be invoked when a {@link BeanOverride @BeanOverride} + * annotation is declared at the type level — for example, if the supplied + * test class is annotated with {@code @MockitoBean}. + *

    Note that the test class may not be directly annotated with the override + * annotation. For example, the override annotation may have been declared + * on an interface, superclass, or enclosing class within the test class + * hierarchy. + *

    The default implementation returns an empty list, signaling that this + * {@code BeanOverrideProcessor} does not support type-level {@code @BeanOverride} + * declarations. Can be overridden by concrete implementations to support + * type-level use cases. + * @param overrideAnnotation the composed annotation that declares the + * {@code @BeanOverride} annotation which registers this processor + * @param testClass the test class to process + * @return the list of {@code BeanOverrideHandlers} for the annotated class + * @since 6.2.2 + * @see #createHandler(Annotation, Class, Field) + */ + default List createHandlers(Annotation overrideAnnotation, Class testClass) { + return Collections.emptyList(); + } + } diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistry.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistry.java index b89c4e7e5318..3afc7c885af1 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistry.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistry.java @@ -108,9 +108,11 @@ Object wrapBeanIfNecessary(Object bean, String beanName) { } void inject(Object target, BeanOverrideHandler handler) { + Field field = handler.getField(); + Assert.notNull(field, () -> "BeanOverrideHandler must have a non-null field: " + handler); String beanName = this.handlerToBeanNameMap.get(handler); Assert.state(StringUtils.hasLength(beanName), () -> "No bean found for BeanOverrideHandler: " + handler); - inject(handler.getField(), target, beanName); + inject(field, target, beanName); } private void inject(Field field, Object target, String beanName) { diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/AbstractMockitoBeanOverrideHandler.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/AbstractMockitoBeanOverrideHandler.java index 367c43a59c36..3c9454ce9904 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/AbstractMockitoBeanOverrideHandler.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/AbstractMockitoBeanOverrideHandler.java @@ -38,7 +38,7 @@ abstract class AbstractMockitoBeanOverrideHandler extends BeanOverrideHandler { private final MockReset reset; - protected AbstractMockitoBeanOverrideHandler(Field field, ResolvableType beanType, + protected AbstractMockitoBeanOverrideHandler(@Nullable Field field, ResolvableType beanType, @Nullable String beanName, BeanOverrideStrategy strategy, @Nullable MockReset reset) { super(field, beanType, beanName, strategy); diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java index 640a75bb22e9..7d11bd813c22 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -29,17 +30,37 @@ import org.springframework.test.context.bean.override.BeanOverride; /** - * {@code @MockitoBean} is an annotation that can be applied to a non-static field - * in a test class to override a bean in the test's + * {@code @MockitoBean} is an annotation that can be used in test classes to + * override beans in a test's * {@link org.springframework.context.ApplicationContext ApplicationContext} - * using a Mockito mock. + * using Mockito mocks. * - *

    By default, the bean to mock is inferred from the type of the annotated - * field. If multiple candidates exist, a {@code @Qualifier} annotation can be - * used to help disambiguate. In the absence of a {@code @Qualifier} annotation, - * the name of the annotated field will be used as a fallback qualifier. - * Alternatively, you can explicitly specify a bean name to mock by setting the - * {@link #value() value} or {@link #name() name} attribute. + *

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

    + * + *

    When {@code @MockitoBean} is declared on a field, the bean to mock is inferred + * from the type of the annotated field. If multiple candidates exist, a + * {@code @Qualifier} annotation can be declared on the field to help disambiguate. + * In the absence of a {@code @Qualifier} annotation, the name of the annotated + * field will be used as a fallback qualifier. Alternatively, you can explicitly + * specify a bean name to mock by setting the {@link #value() value} or + * {@link #name() name} attribute. + * + *

    When {@code @MockitoBean} is declared at the type level, the type of bean + * to mock must be supplied via the {@link #types() types} attribute. If multiple + * candidates exist, you can explicitly specify a bean name to mock by setting the + * {@link #name() name} attribute. Note, however, that the {@code types} attribute + * must contain a single type if an explicit bean {@code name} is configured. * *

    A bean will be created if a corresponding bean does not exist. However, if * you would like for the test to fail when a corresponding bean does not exist, @@ -63,19 +84,29 @@ * (default visibility), or {@code private} depending on the needs or coding * practices of the project. * - *

    {@code @MockitoBean} fields will be inherited from an enclosing test class by default. - * See {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration} + *

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

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

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

    Defaults to none. + *

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

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

    When {@code @MockitoBean} also defines a {@link #name}, this attribute + * can only contain a single value. + * @return the types to mock + * @since 6.2.2 + */ + Class[] types() default {}; + /** * Extra interfaces that should also be declared by the mock. *

    Defaults to none. diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandler.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandler.java index 190999fb5dad..32a2f4e08096 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandler.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandler.java @@ -57,13 +57,17 @@ class MockitoBeanOverrideHandler extends AbstractMockitoBeanOverrideHandler { private final boolean serializable; - MockitoBeanOverrideHandler(Field field, ResolvableType typeToMock, MockitoBean mockitoBean) { + MockitoBeanOverrideHandler(ResolvableType typeToMock, MockitoBean mockitoBean) { + this(null, typeToMock, mockitoBean); + } + + MockitoBeanOverrideHandler(@Nullable Field field, ResolvableType typeToMock, MockitoBean mockitoBean) { this(field, typeToMock, (!mockitoBean.name().isBlank() ? mockitoBean.name() : null), (mockitoBean.enforceOverride() ? REPLACE : REPLACE_OR_CREATE), mockitoBean.reset(), mockitoBean.extraInterfaces(), mockitoBean.answers(), mockitoBean.serializable()); } - private MockitoBeanOverrideHandler(Field field, ResolvableType typeToMock, @Nullable String beanName, + private MockitoBeanOverrideHandler(@Nullable Field field, ResolvableType typeToMock, @Nullable String beanName, BeanOverrideStrategy strategy, MockReset reset, Class[] extraInterfaces, Answers answers, boolean serializable) { diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessor.java index cc67a9488e64..62c07d594153 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessor.java @@ -18,15 +18,20 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; import org.springframework.core.ResolvableType; +import org.springframework.test.context.bean.override.BeanOverrideHandler; import org.springframework.test.context.bean.override.BeanOverrideProcessor; +import org.springframework.util.Assert; /** * {@link BeanOverrideProcessor} implementation that provides support for * {@link MockitoBean @MockitoBean} and {@link MockitoSpyBean @MockitoSpyBean}. * * @author Simon Baslé + * @author Sam Brannen * @since 6.2 * @see MockitoBean @MockitoBean * @see MockitoSpyBean @MockitoSpyBean @@ -36,6 +41,8 @@ class MockitoBeanOverrideProcessor implements BeanOverrideProcessor { @Override public AbstractMockitoBeanOverrideHandler createHandler(Annotation overrideAnnotation, Class testClass, Field field) { if (overrideAnnotation instanceof MockitoBean mockitoBean) { + Assert.state(mockitoBean.types().length == 0, + "The @MockitoBean 'types' attribute must be omitted when declared on a field"); return new MockitoBeanOverrideHandler(field, ResolvableType.forField(field, testClass), mockitoBean); } else if (overrideAnnotation instanceof MockitoSpyBean spyBean) { @@ -47,4 +54,23 @@ else if (overrideAnnotation instanceof MockitoSpyBean spyBean) { .formatted(field.getDeclaringClass().getName(), field.getName())); } + @Override + public List createHandlers(Annotation overrideAnnotation, Class testClass) { + if (!(overrideAnnotation instanceof MockitoBean mockitoBean)) { + throw new IllegalStateException(""" + Invalid annotation passed to MockitoBeanOverrideProcessor: \ + expected @MockitoBean on test class """ + testClass.getName()); + } + Class[] types = mockitoBean.types(); + Assert.state(types.length > 0, + "The @MockitoBean 'types' attribute must not be empty when declared on a class"); + Assert.state(mockitoBean.name().isEmpty() || types.length == 1, + "The @MockitoBean 'name' attribute cannot be used when mocking multiple types"); + List handlers = new ArrayList<>(); + for (Class type : types) { + handlers.add(new MockitoBeanOverrideHandler(ResolvableType.forClass(type), mockitoBean)); + } + return handlers; + } + } diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeans.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeans.java new file mode 100644 index 000000000000..3bf80b47d13b --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeans.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Container for {@link MockitoBean @MockitoBean} annotations which allows + * {@code @MockitoBean} to be used as a {@linkplain java.lang.annotation.Repeatable + * repeatable annotation} at the type level — for example, on test classes + * or interfaces implemented by test classes. + * + * @author Sam Brannen + * @since 6.2.2 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface MockitoBeans { + + MockitoBean[] value(); + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideTestUtils.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideTestUtils.java index 8932a072c84d..994ffffa35d1 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideTestUtils.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideTestUtils.java @@ -30,4 +30,8 @@ public static List findHandlers(Class testClass) { return BeanOverrideHandler.forTestClass(testClass); } + public static List findAllHandlers(Class testClass) { + return BeanOverrideHandler.findAllHandlers(testClass); + } + } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandlerTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandlerTests.java index 72fc58149dad..466bcd93e348 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandlerTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -67,6 +67,76 @@ void isEqualToWithSameMetadataFromField() { assertThat(handler1).hasSameHashCodeAs(handler2); } + @Test // gh-33925 + void isEqualToWithSameInstanceFromClassLevel() { + MockitoBeanOverrideHandler handler1 = createHandler(ClassLevelStringMockByName1.class); + assertThat(handler1).isEqualTo(handler1); + assertThat(handler1).hasSameHashCodeAs(handler1); + + MockitoBeanOverrideHandler handler2 = createHandler(ClassLevelStringMockByType1.class); + assertThat(handler2).isEqualTo(handler2); + assertThat(handler2).hasSameHashCodeAs(handler2); + } + + @Test // gh-33925 + void isEqualToWithSameByNameLookupMetadataFromClassLevel() { + MockitoBeanOverrideHandler handler1 = createHandler(ClassLevelStringMockByName1.class); + MockitoBeanOverrideHandler handler2 = createHandler(ClassLevelStringMockByName2.class); + assertThat(handler1).isEqualTo(handler2); + assertThat(handler2).isEqualTo(handler1); + assertThat(handler1).hasSameHashCodeAs(handler2); + } + + @Test // gh-33925 + void isNotEqualToWithDifferentByNameLookupMetadataFromClassLevel() { + MockitoBeanOverrideHandler handler1 = createHandler(ClassLevelStringMockByName1.class); + MockitoBeanOverrideHandler handler2 = createHandler(ClassLevelStringMockByName3.class); + assertThat(handler1).isNotEqualTo(handler2); + assertThat(handler2).isNotEqualTo(handler1); + assertThat(handler1).doesNotHaveSameHashCodeAs(handler2); + } + + @Test // gh-33925 + void isEqualToWithSameByTypeLookupMetadataFromClassLevel() { + MockitoBeanOverrideHandler handler1 = createHandler(ClassLevelStringMockByType1.class); + MockitoBeanOverrideHandler handler2 = createHandler(ClassLevelStringMockByType2.class); + assertThat(handler1).isEqualTo(handler2); + assertThat(handler2).isEqualTo(handler1); + assertThat(handler1).hasSameHashCodeAs(handler2); + } + + @Test // gh-33925 + void isNotEqualToWithDifferentByTypeLookupMetadataFromClassLevel() { + MockitoBeanOverrideHandler handler1 = createHandler(ClassLevelStringMockByType1.class); + MockitoBeanOverrideHandler handler2 = createHandler(ClassLevelStringMockByType3.class); + assertThat(handler1).isNotEqualTo(handler2); + assertThat(handler2).isNotEqualTo(handler1); + assertThat(handler1).doesNotHaveSameHashCodeAs(handler2); + } + + @Test // gh-33925 + void isEqualToWithSameByNameLookupMetadataFromFieldAndClassLevel() { + MockitoBeanOverrideHandler handler1 = createHandler(sampleField("service3")); + MockitoBeanOverrideHandler handler2 = createHandler(ClassLevelStringMockByName1.class); + assertThat(handler1).isEqualTo(handler2); + assertThat(handler2).isEqualTo(handler1); + assertThat(handler1).hasSameHashCodeAs(handler2); + } + + /** + * Since the "field name as fallback qualifier" is not available for an annotated class, + * what would seem to be "equivalent" handlers are actually not considered "equal" when + * the the lookup is "by type". + */ + @Test // gh-33925 + void isNotEqualToWithSameByTypeLookupMetadataFromFieldAndClassLevel() { + MockitoBeanOverrideHandler handler1 = createHandler(sampleField("service")); + MockitoBeanOverrideHandler handler2 = createHandler(ClassLevelStringMockByType1.class); + assertThat(handler1).isNotEqualTo(handler2); + assertThat(handler2).isNotEqualTo(handler1); + assertThat(handler1).doesNotHaveSameHashCodeAs(handler2); + } + @Test void isNotEqualEqualToByTypeLookupWithSameMetadataButDifferentField() { MockitoBeanOverrideHandler handler1 = createHandler(sampleField("service")); @@ -122,6 +192,11 @@ private static MockitoBeanOverrideHandler createHandler(Field field) { return new MockitoBeanOverrideHandler(field, ResolvableType.forClass(field.getType()), annotation); } + private MockitoBeanOverrideHandler createHandler(Class clazz) { + MockitoBean annotation = AnnotatedElementUtils.getMergedAnnotation(clazz, MockitoBean.class); + return new MockitoBeanOverrideHandler(null, ResolvableType.forClass(annotation.types()[0]), annotation); + } + static class SampleOneMock { @@ -159,4 +234,28 @@ static class Sample { private String service7; } + @MockitoBean(name = "beanToMock", types = String.class) + static class ClassLevelStringMockByName1 { + } + + @MockitoBean(name = "beanToMock", types = String.class) + static class ClassLevelStringMockByName2 { + } + + @MockitoBean(name = "otherBeanToMock", types = String.class) + static class ClassLevelStringMockByName3 { + } + + @MockitoBean(types = String.class) + static class ClassLevelStringMockByType1 { + } + + @MockitoBean(types = String.class) + static class ClassLevelStringMockByType2 { + } + + @MockitoBean(types = Integer.class) + static class ClassLevelStringMockByType3 { + } + } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessorTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessorTests.java index d25475990b45..7f4d07c25c44 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessorTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessorTests.java @@ -18,7 +18,9 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Field; +import java.util.List; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.core.annotation.AnnotationUtils; @@ -41,43 +43,155 @@ class MockitoBeanOverrideProcessorTests { private final MockitoBeanOverrideProcessor processor = new MockitoBeanOverrideProcessor(); - private final Field field = ReflectionUtils.findField(TestCase.class, "number"); + @Nested + class CreateHandlerTests { + private final Field field = ReflectionUtils.findField(TestCase.class, "number"); - @Test - void mockAnnotationCreatesMockitoBeanOverrideHandler() { - MockitoBean annotation = AnnotationUtils.synthesizeAnnotation(MockitoBean.class); - BeanOverrideHandler object = processor.createHandler(annotation, TestCase.class, field); - assertThat(object).isExactlyInstanceOf(MockitoBeanOverrideHandler.class); - } + @Test + void mockAnnotationCreatesMockitoBeanOverrideHandler() { + MockitoBean annotation = AnnotationUtils.synthesizeAnnotation(MockitoBean.class); + BeanOverrideHandler object = processor.createHandler(annotation, TestCase.class, field); - @Test - void spyAnnotationCreatesMockitoSpyBeanOverrideHandler() { - MockitoSpyBean annotation = AnnotationUtils.synthesizeAnnotation(MockitoSpyBean.class); - BeanOverrideHandler object = processor.createHandler(annotation, TestCase.class, field); + assertThat(object).isExactlyInstanceOf(MockitoBeanOverrideHandler.class); + } - assertThat(object).isExactlyInstanceOf(MockitoSpyBeanOverrideHandler.class); - } + @Test + void spyAnnotationCreatesMockitoSpyBeanOverrideHandler() { + MockitoSpyBean annotation = AnnotationUtils.synthesizeAnnotation(MockitoSpyBean.class); + BeanOverrideHandler object = processor.createHandler(annotation, TestCase.class, field); + + assertThat(object).isExactlyInstanceOf(MockitoSpyBeanOverrideHandler.class); + } + + @Test + void otherAnnotationThrows() { + Annotation annotation = field.getAnnotation(Nullable.class); + + assertThatIllegalStateException() + .isThrownBy(() -> processor.createHandler(annotation, TestCase.class, field)) + .withMessage("Invalid annotation passed to MockitoBeanOverrideProcessor: expected either " + + "@MockitoBean or @MockitoSpyBean on field %s.%s", field.getDeclaringClass().getName(), + field.getName()); + } + + @Test + void typesNotSupportedAtFieldLevel() { + Field field = ReflectionUtils.findField(TestCase.class, "typesNotSupported"); + MockitoBean annotation = field.getAnnotation(MockitoBean.class); + + assertThatIllegalStateException() + .isThrownBy(() -> processor.createHandler(annotation, TestCase.class, field)) + .withMessage("The @MockitoBean 'types' attribute must be omitted when declared on a field"); + } - @Test - void otherAnnotationThrows() { - Annotation annotation = field.getAnnotation(Nullable.class); - assertThatIllegalStateException() - .isThrownBy(() -> processor.createHandler(annotation, TestCase.class, field)) - .withMessage("Invalid annotation passed to MockitoBeanOverrideProcessor: expected either " + - "@MockitoBean or @MockitoSpyBean on field %s.%s", field.getDeclaringClass().getName(), - field.getName()); + static class TestCase { + + @Nullable + @MockitoBean + @MockitoSpyBean + Integer number; + + @MockitoBean(types = Integer.class) + String typesNotSupported; + } + + @MockitoBean(name = "bogus", types = Integer.class) + static class NameNotSupportedTestCase { + } } + @Nested + class CreateHandlersTests { + + @Test + void missingTypes() { + Class testClass = MissingTypesTestCase.class; + MockitoBean annotation = testClass.getAnnotation(MockitoBean.class); + + assertThatIllegalStateException() + .isThrownBy(() -> processor.createHandlers(annotation, testClass)) + .withMessage("The @MockitoBean 'types' attribute must not be empty when declared on a class"); + } + + @Test + void nameNotSupportedWithMultipleTypes() { + Class testClass = NameNotSupportedWithMultipleTypesTestCase.class; + MockitoBean annotation = testClass.getAnnotation(MockitoBean.class); + + assertThatIllegalStateException() + .isThrownBy(() -> processor.createHandlers(annotation, testClass)) + .withMessage("The @MockitoBean 'name' attribute cannot be used when mocking multiple types"); + } + + @Test + void singleMockByType() { + Class testClass = SingleMockByTypeTestCase.class; + MockitoBean annotation = testClass.getAnnotation(MockitoBean.class); + List handlers = processor.createHandlers(annotation, testClass); + + assertThat(handlers).singleElement().isInstanceOfSatisfying(MockitoBeanOverrideHandler.class, handler -> { + assertThat(handler.getField()).isNull(); + assertThat(handler.getBeanName()).isNull(); + assertThat(handler.getBeanType().resolve()).isEqualTo(Integer.class); + }); + } + + @Test + void singleMockByName() { + Class testClass = SingleMockByNameTestCase.class; + MockitoBean annotation = testClass.getAnnotation(MockitoBean.class); + List handlers = processor.createHandlers(annotation, testClass); + + assertThat(handlers).singleElement().isInstanceOfSatisfying(MockitoBeanOverrideHandler.class, handler -> { + assertThat(handler.getField()).isNull(); + assertThat(handler.getBeanName()).isEqualTo("enigma"); + assertThat(handler.getBeanType().resolve()).isEqualTo(Integer.class); + }); + } + + @Test + void multipleMocks() { + Class testClass = MultipleMocksTestCase.class; + MockitoBean annotation = testClass.getAnnotation(MockitoBean.class); + List handlers = processor.createHandlers(annotation, testClass); + + assertThat(handlers).satisfiesExactly( + handler1 -> { + assertThat(handler1.getField()).isNull(); + assertThat(handler1.getBeanName()).isNull(); + assertThat(handler1.getBeanType().resolve()).isEqualTo(Integer.class); + }, + handler2 -> { + assertThat(handler2.getField()).isNull(); + assertThat(handler2.getBeanName()).isNull(); + assertThat(handler2.getBeanType().resolve()).isEqualTo(Float.class); + } + ); + } - static class TestCase { - @Nullable @MockitoBean - @MockitoSpyBean - Integer number; + static class MissingTypesTestCase { + } + + @MockitoBean(name = "bogus", types = { Integer.class, Float.class }) + static class NameNotSupportedWithMultipleTypesTestCase { + } + + @MockitoBean(types = Integer.class) + static class SingleMockByTypeTestCase { + } + + @MockitoBean(name = "enigma", types = Integer.class) + static class SingleMockByNameTestCase { + } + + @MockitoBean(types = { Integer.class, Float.class }) + static class MultipleMocksTestCase { + } } } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/MockitoBeansByNameIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/MockitoBeansByNameIntegrationTests.java new file mode 100644 index 000000000000..b27e2dd860af --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/MockitoBeansByNameIntegrationTests.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.mockbeans; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.bean.override.mockito.MockitoBeans; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; + +/** + * Integration tests for {@link MockitoBeans @MockitoBeans} and + * {@link MockitoBean @MockitoBean} declared "by name" at the class level as a + * repeatable annotation. + * + * @author Sam Brannen + * @since 6.2.2 + * @see gh-33925 + * @see MockitoBeansByTypeIntegrationTests + */ +@SpringJUnitConfig +@MockitoBean(name = "s1", types = ExampleService.class) +@MockitoBean(name = "s2", types = ExampleService.class) +class MockitoBeansByNameIntegrationTests { + + @Autowired + ExampleService s1; + + @Autowired + ExampleService s2; + + @MockitoBean(name = "s3") + ExampleService service3; + + @Autowired + @Qualifier("s4") + ExampleService service4; + + + @BeforeEach + void configureMocks() { + given(s1.greeting()).willReturn("mock 1"); + given(s2.greeting()).willReturn("mock 2"); + given(service3.greeting()).willReturn("mock 3"); + } + + @Test + void checkMocksAndStandardBean() { + assertThat(s1.greeting()).isEqualTo("mock 1"); + assertThat(s2.greeting()).isEqualTo("mock 2"); + assertThat(service3.greeting()).isEqualTo("mock 3"); + assertThat(service4.greeting()).isEqualTo("prod 4"); + } + + @Configuration + static class Config { + + @Bean + ExampleService s1() { + return () -> "prod 1"; + } + + @Bean + ExampleService s2() { + return () -> "prod 2"; + } + + @Bean + ExampleService s3() { + return () -> "prod 3"; + } + + @Bean + ExampleService s4() { + return () -> "prod 4"; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/MockitoBeansByTypeIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/MockitoBeansByTypeIntegrationTests.java new file mode 100644 index 000000000000..c70fc89f6120 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/MockitoBeansByTypeIntegrationTests.java @@ -0,0 +1,147 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.mockbeans; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.bean.override.mockito.MockitoBeans; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; + +/** + * Integration tests for {@link MockitoBeans @MockitoBeans} and + * {@link MockitoBean @MockitoBean} declared "by type" at the class level, as a + * repeatable annotation, and via a custom composed annotation. + * + * @author Sam Brannen + * @since 6.2.2 + * @see gh-33925 + * @see MockitoBeansByNameIntegrationTests + */ +@SpringJUnitConfig +@MockitoBean(types = {Service04.class, Service05.class}) +@SharedMocks // Intentionally declared between local @MockitoBean declarations +@MockitoBean(types = Service06.class) +class MockitoBeansByTypeIntegrationTests implements TestInterface01 { + + @Autowired + Service01 service01; + + @Autowired + Service02 service02; + + @Autowired + Service03 service03; + + @Autowired + Service04 service04; + + @Autowired + Service05 service05; + + @Autowired + Service06 service06; + + @MockitoBean + Service07 service07; + + + @BeforeEach + void configureMocks() { + given(service01.greeting()).willReturn("mock 01"); + given(service02.greeting()).willReturn("mock 02"); + given(service03.greeting()).willReturn("mock 03"); + given(service04.greeting()).willReturn("mock 04"); + given(service05.greeting()).willReturn("mock 05"); + given(service06.greeting()).willReturn("mock 06"); + given(service07.greeting()).willReturn("mock 07"); + } + + @Test + void checkMocks() { + assertThat(service01.greeting()).isEqualTo("mock 01"); + assertThat(service02.greeting()).isEqualTo("mock 02"); + assertThat(service03.greeting()).isEqualTo("mock 03"); + assertThat(service04.greeting()).isEqualTo("mock 04"); + assertThat(service05.greeting()).isEqualTo("mock 05"); + assertThat(service06.greeting()).isEqualTo("mock 06"); + assertThat(service07.greeting()).isEqualTo("mock 07"); + } + + + @MockitoBean(types = Service09.class) + static class BaseTestCase implements TestInterface08 { + + @Autowired + Service08 service08; + + @Autowired + Service09 service09; + + @MockitoBean + Service10 service10; + } + + @Nested + @MockitoBean(types = Service12.class) + class NestedTests extends BaseTestCase implements TestInterface11 { + + @Autowired + Service11 service11; + + @Autowired + Service12 service12; + + @MockitoBean + Service13 service13; + + + @BeforeEach + void configureMocks() { + given(service08.greeting()).willReturn("mock 08"); + given(service09.greeting()).willReturn("mock 09"); + given(service10.greeting()).willReturn("mock 10"); + given(service11.greeting()).willReturn("mock 11"); + given(service12.greeting()).willReturn("mock 12"); + given(service13.greeting()).willReturn("mock 13"); + } + + @Test + void checkMocks() { + assertThat(service01.greeting()).isEqualTo("mock 01"); + assertThat(service02.greeting()).isEqualTo("mock 02"); + assertThat(service03.greeting()).isEqualTo("mock 03"); + assertThat(service04.greeting()).isEqualTo("mock 04"); + assertThat(service05.greeting()).isEqualTo("mock 05"); + assertThat(service06.greeting()).isEqualTo("mock 06"); + assertThat(service07.greeting()).isEqualTo("mock 07"); + assertThat(service08.greeting()).isEqualTo("mock 08"); + assertThat(service09.greeting()).isEqualTo("mock 09"); + assertThat(service10.greeting()).isEqualTo("mock 10"); + assertThat(service11.greeting()).isEqualTo("mock 11"); + assertThat(service12.greeting()).isEqualTo("mock 12"); + assertThat(service13.greeting()).isEqualTo("mock 13"); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/MockitoBeansTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/MockitoBeansTests.java new file mode 100644 index 000000000000..3ef3e14ea2e2 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/MockitoBeansTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.mockbeans; + +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.ResolvableType; +import org.springframework.test.context.bean.override.BeanOverrideHandler; +import org.springframework.test.context.bean.override.BeanOverrideTestUtils; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.bean.override.mockito.MockitoBeans; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MockitoBeans @MockitoBeans}: {@link MockitoBean @MockitoBean} + * declared at the class level, as a repeatable annotation, and via a custom composed + * annotation. + * + * @author Sam Brannen + * @since 6.2.2 + * @see gh-33925 + */ +class MockitoBeansTests { + + @Test + void registrationOrderForTopLevelClass() { + Stream> mockedServices = getRegisteredMockTypes(MockitoBeansByTypeIntegrationTests.class); + assertThat(mockedServices).containsExactly( + Service01.class, Service02.class, Service03.class, Service04.class, + Service05.class, Service06.class, Service07.class); + } + + @Test + void registrationOrderForNestedClass() { + Stream> mockedServices = getRegisteredMockTypes(MockitoBeansByTypeIntegrationTests.NestedTests.class); + assertThat(mockedServices).containsExactly( + Service01.class, Service02.class, Service03.class, Service04.class, + Service05.class, Service06.class, Service07.class, Service08.class, + Service09.class, Service10.class, Service11.class, Service12.class, + Service13.class); + } + + + private static Stream> getRegisteredMockTypes(Class testClass) { + return BeanOverrideTestUtils.findAllHandlers(testClass) + .stream() + .map(BeanOverrideHandler::getBeanType) + .map(ResolvableType::getRawClass); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service.java new file mode 100644 index 000000000000..187ffeb6a833 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service.java @@ -0,0 +1,21 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.mockbeans; + +interface Service { + String greeting(); +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service01.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service01.java new file mode 100644 index 000000000000..a2110bf7fb5a --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service01.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.mockbeans; + +interface Service01 extends Service { +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service02.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service02.java new file mode 100644 index 000000000000..c2b62a558eec --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service02.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.mockbeans; + +interface Service02 extends Service { +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service03.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service03.java new file mode 100644 index 000000000000..31fe690f5944 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service03.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.mockbeans; + +interface Service03 extends Service { +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service04.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service04.java new file mode 100644 index 000000000000..d32ba233d478 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service04.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.mockbeans; + +interface Service04 extends Service { +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service05.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service05.java new file mode 100644 index 000000000000..8c738aa36dde --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service05.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.mockbeans; + +interface Service05 extends Service { +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service06.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service06.java new file mode 100644 index 000000000000..bceab3996501 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service06.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.mockbeans; + +interface Service06 extends Service { +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service07.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service07.java new file mode 100644 index 000000000000..35e82c9fdad8 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service07.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.mockbeans; + +interface Service07 extends Service { +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service08.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service08.java new file mode 100644 index 000000000000..d9630716cf73 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service08.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.mockbeans; + +interface Service08 extends Service { +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service09.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service09.java new file mode 100644 index 000000000000..cb5e92242eee --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service09.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.mockbeans; + +interface Service09 extends Service { +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service10.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service10.java new file mode 100644 index 000000000000..50231a138491 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service10.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.mockbeans; + +interface Service10 extends Service { +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service11.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service11.java new file mode 100644 index 000000000000..3b9203d5ee88 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service11.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.mockbeans; + +interface Service11 extends Service { +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service12.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service12.java new file mode 100644 index 000000000000..097d5ade6c47 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service12.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.mockbeans; + +interface Service12 extends Service { +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service13.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service13.java new file mode 100644 index 000000000000..233e52be6f25 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/Service13.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.mockbeans; + +interface Service13 extends Service { +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/SharedMocks.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/SharedMocks.java new file mode 100644 index 000000000000..e44017dc6af1 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/SharedMocks.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.mockbeans; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@MockitoBean(types = Service02.class) +@MockitoBean(types = Service03.class) +@interface SharedMocks { +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/TestInterface01.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/TestInterface01.java new file mode 100644 index 000000000000..a28e7d495984 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/TestInterface01.java @@ -0,0 +1,23 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.mockbeans; + +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +@MockitoBean(types = Service01.class) +interface TestInterface01 { +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/TestInterface08.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/TestInterface08.java new file mode 100644 index 000000000000..45d326882210 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/TestInterface08.java @@ -0,0 +1,23 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.mockbeans; + +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +@MockitoBean(types = Service08.class) +interface TestInterface08 { +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/TestInterface11.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/TestInterface11.java new file mode 100644 index 000000000000..2bfe47e028b4 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/mockbeans/TestInterface11.java @@ -0,0 +1,23 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.mockbeans; + +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +@MockitoBean(types = Service11.class) +interface TestInterface11 { +}