Support @⁠MockitoBean and @⁠MockitoSpyBean on test constructor parameters

Prior to this commit, @⁠MockitoBean and @⁠MockitoSpyBean could be
declared on fields or at the type level (on test classes and test
interfaces), but not on constructor parameters. Consequently, a test
class could not use constructor injection for bean overrides.

To address that, this commit introduces support for @⁠MockitoBean and
@⁠MockitoSpyBean on constructor parameters in JUnit Jupiter test
classes. Specifically, the Bean Override infrastructure has been
overhauled to support constructor parameters as declaration sites and
injection points alongside fields, and the SpringExtension now
recognizes composed @⁠BeanOverride annotations on constructor
parameters in supportsParameter() and resolves them properly in
resolveParameter(). Note, however, that this support has not been
introduced for @⁠TestBean.

For example, the following which uses field injection:

   @⁠SpringJUnitConfig(TestConfig.class)
   class BeanOverrideTests {

      @⁠MockitoBean
      CustomService customService;

      // tests...
   }

Can now be rewritten to use constructor injection:

   @⁠SpringJUnitConfig(TestConfig.class)
   class BeanOverrideTests {

      private final CustomService customService;

      BeanOverrideTests(@⁠MockitoBean CustomService customService) {
         this.customService = customService;
      }

      // tests...
   }

With Kotlin this can be achieved even more succinctly via a compact
constructor declaration:

   @⁠SpringJUnitConfig(TestConfig::class)
   class BeanOverrideTests(@⁠MockitoBean val customService: CustomService) {

      // tests...
   }

Of course, if one is a fan of so-called "test records", that can also
be achieved succinctly with a Java record:

   @⁠SpringJUnitConfig(TestConfig.class)
   record BeanOverrideTests(@⁠MockitoBean CustomService customService) {

      // tests...
   }

Closes gh-36096
This commit is contained in:
Sam Brannen
2026-03-29 17:17:16 +02:00
parent 955f9d3ea9
commit f9523a785b
31 changed files with 1730 additions and 112 deletions
@@ -24,7 +24,7 @@ import java.lang.annotation.Target;
import org.springframework.beans.factory.annotation.Qualifier;
@Target({ElementType.FIELD, ElementType.METHOD})
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Qualifier
@@ -88,6 +88,65 @@ class MockitoBeanConfigurationErrorTests {
List.of("bean1", "bean2"));
}
@Test // gh-36096
void cannotOverrideBeanByNameWithNoSuchBeanNameOnConstructorParameter() {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean("anotherBean", String.class, () -> "example");
BeanOverrideContextCustomizerTestUtils.customizeApplicationContext(FailureByNameLookupOnConstructorParameter.class, context);
assertThatIllegalStateException()
.isThrownBy(context::refresh)
.withMessage("""
Unable to replace bean: there is no bean with name 'beanToOverride' and type \
java.lang.String (as required by parameter 'example' in constructor for %s). \
If the bean is defined in a @Bean method, make sure the return type is the most \
specific type possible (for example, the concrete implementation type).""",
FailureByNameLookupOnConstructorParameter.class.getName());
}
@Test // gh-36096
void cannotOverrideBeanByNameWithBeanOfWrongTypeOnConstructorParameter() {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean("beanToOverride", Integer.class, () -> 42);
BeanOverrideContextCustomizerTestUtils.customizeApplicationContext(FailureByNameLookupOnConstructorParameter.class, context);
assertThatIllegalStateException()
.isThrownBy(context::refresh)
.withMessage("""
Unable to replace bean: there is no bean with name 'beanToOverride' and type \
java.lang.String (as required by parameter 'example' in constructor for %s). \
If the bean is defined in a @Bean method, make sure the return type is the most \
specific type possible (for example, the concrete implementation type).""",
FailureByNameLookupOnConstructorParameter.class.getName());
}
@Test // gh-36096
void cannotOverrideBeanByTypeWithNoSuchBeanTypeOnConstructorParameter() {
GenericApplicationContext context = new GenericApplicationContext();
BeanOverrideContextCustomizerTestUtils.customizeApplicationContext(FailureByTypeLookupOnConstructorParameter.class, context);
assertThatIllegalStateException()
.isThrownBy(context::refresh)
.withMessage("""
Unable to override bean: there are no beans of type java.lang.String \
(as required by parameter 'example' in constructor for %s). \
If the bean is defined in a @Bean method, make sure the return type is the most \
specific type possible (for example, the concrete implementation type).""",
FailureByTypeLookupOnConstructorParameter.class.getName());
}
@Test // gh-36096
void cannotOverrideBeanByTypeWithTooManyBeansOfThatTypeOnConstructorParameter() {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean("bean1", String.class, () -> "example1");
context.registerBean("bean2", String.class, () -> "example2");
BeanOverrideContextCustomizerTestUtils.customizeApplicationContext(FailureByTypeLookupOnConstructorParameter.class, context);
assertThatIllegalStateException()
.isThrownBy(context::refresh)
.withMessage("""
Unable to select a bean to override: found 2 beans of type java.lang.String \
(as required by parameter 'example' in constructor for %s): %s""",
FailureByTypeLookupOnConstructorParameter.class.getName(),
List.of("bean1", "bean2"));
}
static class FailureByTypeLookup {
@@ -99,7 +158,18 @@ class MockitoBeanConfigurationErrorTests {
@MockitoBean(name = "beanToOverride", enforceOverride = true)
String example;
}
static class FailureByTypeLookupOnConstructorParameter {
FailureByTypeLookupOnConstructorParameter(@MockitoBean(enforceOverride = true) String example) {
}
}
static class FailureByNameLookupOnConstructorParameter {
FailureByNameLookupOnConstructorParameter(@MockitoBean(name = "beanToOverride", enforceOverride = true) String example) {
}
}
}
@@ -194,7 +194,7 @@ class MockitoBeanOverrideHandlerTests {
private MockitoBeanOverrideHandler createHandler(Class<?> clazz) {
MockitoBean annotation = AnnotatedElementUtils.getMergedAnnotation(clazz, MockitoBean.class);
return new MockitoBeanOverrideHandler(null, ResolvableType.forClass(annotation.types()[0]), annotation);
return new MockitoBeanOverrideHandler((Field) null, ResolvableType.forClass(annotation.types()[0]), annotation);
}
@@ -18,6 +18,7 @@ package org.springframework.test.context.bean.override.mockito;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Parameter;
import java.util.List;
import org.jspecify.annotations.Nullable;
@@ -102,6 +103,81 @@ class MockitoBeanOverrideProcessorTests {
}
}
@Nested // gh-36096
class CreateHandlerForParameterTests {
private final Parameter parameter = TestCase.class.getDeclaredConstructors()[0].getParameters()[0];
@Test
void mockAnnotationCreatesMockitoBeanOverrideHandler() {
MockitoBean annotation = AnnotationUtils.synthesizeAnnotation(MockitoBean.class);
BeanOverrideHandler handler = processor.createHandler(annotation, TestCase.class, parameter);
assertThat(handler).isExactlyInstanceOf(MockitoBeanOverrideHandler.class);
}
@Test
void spyAnnotationCreatesMockitoSpyBeanOverrideHandler() {
MockitoSpyBean annotation = AnnotationUtils.synthesizeAnnotation(MockitoSpyBean.class);
BeanOverrideHandler handler = processor.createHandler(annotation, TestCase.class, parameter);
assertThat(handler).isExactlyInstanceOf(MockitoSpyBeanOverrideHandler.class);
}
@Test
void otherAnnotationThrows() {
Annotation annotation = parameter.getAnnotation(Nullable.class);
assertThatIllegalStateException()
.isThrownBy(() -> processor.createHandler(annotation, TestCase.class, parameter))
.withMessage("Invalid annotation passed to MockitoBeanOverrideProcessor: expected either " +
"@MockitoBean or @MockitoSpyBean on parameter '%s' in constructor %s",
parameter.getName(), parameter.getDeclaringExecutable().getName());
}
@Test
void typesAttributeNotSupportedForMockitoBean() {
Parameter parameter = TypesNotSupportedForMockitoBeanTestCase.class
.getDeclaredConstructors()[0].getParameters()[0];
MockitoBean annotation = parameter.getAnnotation(MockitoBean.class);
assertThatIllegalStateException()
.isThrownBy(() -> processor.createHandler(annotation, TypesNotSupportedForMockitoBeanTestCase.class, parameter))
.withMessage("The @MockitoBean 'types' attribute must be omitted when declared on a parameter");
}
@Test
void typesAttributeNotSupportedForMockitoSpyBean() {
Parameter parameter = TypesNotSupportedForMockitoSpyBeanTestCase.class
.getDeclaredConstructors()[0].getParameters()[0];
MockitoSpyBean annotation = parameter.getAnnotation(MockitoSpyBean.class);
assertThatIllegalStateException()
.isThrownBy(() -> processor.createHandler(annotation, TypesNotSupportedForMockitoSpyBeanTestCase.class, parameter))
.withMessage("The @MockitoSpyBean 'types' attribute must be omitted when declared on a parameter");
}
static class TestCase {
TestCase(@MockitoBean @MockitoSpyBean @Nullable Integer number) {
}
}
static class TypesNotSupportedForMockitoBeanTestCase {
TypesNotSupportedForMockitoBeanTestCase(@MockitoBean(types = Integer.class) String param) {
}
}
static class TypesNotSupportedForMockitoSpyBeanTestCase {
TypesNotSupportedForMockitoSpyBeanTestCase(@MockitoSpyBean(types = Integer.class) String param) {
}
}
}
@Nested
class CreateHandlersTests {
@@ -113,6 +113,50 @@ class MockitoSpyBeanConfigurationErrorTests {
to spy on a scoped proxy, which is not supported.""");
}
@Test // gh-36096
void contextCustomizerCannotBeCreatedWithNoSuchBeanNameOnConstructorParameter() {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean("present", String.class, () -> "example");
BeanOverrideContextCustomizerTestUtils.customizeApplicationContext(ByNameSingleLookupOnConstructorParameter.class, context);
assertThatIllegalStateException()
.isThrownBy(context::refresh)
.withMessage("""
Unable to wrap bean: there is no bean with name 'beanToSpy' and type \
java.lang.String (as required by parameter 'example' in constructor for %s). \
If the bean is defined in a @Bean method, make sure the return type is the most \
specific type possible (for example, the concrete implementation type).""",
ByNameSingleLookupOnConstructorParameter.class.getName());
}
@Test // gh-36096
void contextCustomizerCannotBeCreatedWithNoSuchBeanTypeOnConstructorParameter() {
GenericApplicationContext context = new GenericApplicationContext();
BeanOverrideContextCustomizerTestUtils.customizeApplicationContext(ByTypeSingleLookupOnConstructorParameter.class, context);
assertThatIllegalStateException()
.isThrownBy(context::refresh)
.withMessage("""
Unable to select a bean to wrap: there are no beans of type java.lang.String \
(as required by parameter 'example' in constructor for %s). \
If the bean is defined in a @Bean method, make sure the return type is the most \
specific type possible (for example, the concrete implementation type).""",
ByTypeSingleLookupOnConstructorParameter.class.getName());
}
@Test // gh-36096
void contextCustomizerCannotBeCreatedWithTooManyBeansOfThatTypeOnConstructorParameter() {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean("bean1", String.class, () -> "example1");
context.registerBean("bean2", String.class, () -> "example2");
BeanOverrideContextCustomizerTestUtils.customizeApplicationContext(ByTypeSingleLookupOnConstructorParameter.class, context);
assertThatIllegalStateException()
.isThrownBy(context::refresh)
.withMessage("""
Unable to select a bean to wrap: found 2 beans of type java.lang.String \
(as required by parameter 'example' in constructor for %s): %s""",
ByTypeSingleLookupOnConstructorParameter.class.getName(),
List.of("bean1", "bean2"));
}
static class ByTypeSingleLookup {
@@ -124,7 +168,18 @@ class MockitoSpyBeanConfigurationErrorTests {
@MockitoSpyBean("beanToSpy")
String example;
}
static class ByTypeSingleLookupOnConstructorParameter {
ByTypeSingleLookupOnConstructorParameter(@MockitoSpyBean String example) {
}
}
static class ByNameSingleLookupOnConstructorParameter {
ByNameSingleLookupOnConstructorParameter(@MockitoSpyBean("beanToSpy") String example) {
}
}
static class ScopedProxyTestCase {
@@ -0,0 +1,175 @@
/*
* Copyright 2002-present 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.constructor;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
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.example.RealExampleService;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.test.mockito.MockitoAssertions;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link MockitoBean @MockitoBean} that use by-name lookup
* on constructor parameters.
*
* @author Sam Brannen
* @since 7.1
* @see <a href="https://github.com/spring-projects/spring-framework/issues/36096">gh-36096</a>
* @see org.springframework.test.context.bean.override.mockito.MockitoBeanByNameLookupTestMethodScopedExtensionContextIntegrationTests
*/
@SpringJUnitConfig
class MockitoBeanByNameLookupForConstructorParametersIntegrationTests {
final ExampleService service0A;
final ExampleService service0B;
final ExampleService service0C;
final ExampleService nonExisting;
MockitoBeanByNameLookupForConstructorParametersIntegrationTests(
@MockitoBean ExampleService s0A,
@MockitoBean(name = "s0B") ExampleService service0B,
@MockitoBean @Qualifier("s0C") ExampleService service0C,
@MockitoBean("nonExistingBean") ExampleService nonExisting) {
this.service0A = s0A;
this.service0B = service0B;
this.service0C = service0C;
this.nonExisting = nonExisting;
}
@Test
void parameterNameIsUsedAsBeanName(ApplicationContext ctx) {
assertThat(this.service0A)
.satisfies(MockitoAssertions::assertIsMock)
.isSameAs(ctx.getBean("s0A"));
assertThat(this.service0A.greeting()).as("mocked greeting").isNull();
}
@Test
void explicitBeanNameOverridesParameterName(ApplicationContext ctx) {
assertThat(this.service0B)
.satisfies(MockitoAssertions::assertIsMock)
.isSameAs(ctx.getBean("s0B"));
assertThat(this.service0B.greeting()).as("mocked greeting").isNull();
}
@Test
void qualifierIsUsedToResolveByName(ApplicationContext ctx) {
assertThat(this.service0C)
.satisfies(MockitoAssertions::assertIsMock)
.isSameAs(ctx.getBean("s0C"));
assertThat(this.service0C.greeting()).as("mocked greeting").isNull();
}
@Test
void mockIsCreatedWhenNoBeanExistsWithProvidedName(ApplicationContext ctx) {
assertThat(this.nonExisting)
.satisfies(MockitoAssertions::assertIsMock)
.isSameAs(ctx.getBean("nonExistingBean"));
assertThat(this.nonExisting.greeting()).as("mocked greeting").isNull();
}
@Nested
class NestedTests {
@Autowired
@Qualifier("s0A")
ExampleService localService0A;
@Autowired
@Qualifier("nonExistingBean")
ExampleService localNonExisting;
final ExampleService nestedNonExisting;
NestedTests(@MockitoBean("nestedNonExistingBean") ExampleService nestedNonExisting) {
this.nestedNonExisting = nestedNonExisting;
}
@Test
void mockFromEnclosingClassIsAccessibleViaAutowiring(ApplicationContext ctx) {
assertThat(this.localService0A)
.satisfies(MockitoAssertions::assertIsMock)
.isSameAs(service0A)
.isSameAs(ctx.getBean("s0A"));
assertThat(this.localService0A.greeting()).as("mocked greeting").isNull();
}
@Test
void mockForNonExistingBeanFromEnclosingClassIsAccessibleViaAutowiring(ApplicationContext ctx) {
assertThat(this.localNonExisting)
.satisfies(MockitoAssertions::assertIsMock)
.isSameAs(nonExisting)
.isSameAs(ctx.getBean("nonExistingBean"));
assertThat(this.localNonExisting.greeting()).as("mocked greeting").isNull();
}
@Test
void nestedConstructorParameterIsMockedWhenNoBeanExistsWithProvidedName(ApplicationContext ctx) {
assertThat(this.nestedNonExisting)
.satisfies(MockitoAssertions::assertIsMock)
.isSameAs(ctx.getBean("nestedNonExistingBean"));
assertThat(this.nestedNonExisting.greeting()).as("mocked greeting").isNull();
}
}
@Configuration(proxyBeanMethods = false)
static class Config {
@Bean
ExampleService s0A() {
return new RealExampleService("prod s0A");
}
@Bean
ExampleService s0B() {
return new RealExampleService("prod s0B");
}
@Bean
ExampleService s0C() {
return new RealExampleService("prod s0C");
}
}
}
@@ -0,0 +1,55 @@
/*
* Copyright 2002-present 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.constructor;
import org.junit.jupiter.api.Test;
import org.springframework.test.context.bean.override.example.ExampleService;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.springframework.test.mockito.MockitoAssertions.assertIsMock;
/**
* Integration tests for {@link MockitoBean @MockitoBean} that use by-type lookup
* on constructor parameters in a Java record.
*
* @author Sam Brannen
* @since 7.1
* @see <a href="https://github.com/spring-projects/spring-framework/issues/36096">gh-36096</a>
*/
@SpringJUnitConfig
record MockitoBeanByTypeLookupForConstructorParametersIntegrationRecordTests(
@MockitoBean ExampleService exampleService) {
@Test
void test() {
assertIsMock(this.exampleService);
when(this.exampleService.greeting()).thenReturn("Mocked greeting");
assertThat(this.exampleService.greeting()).isEqualTo("Mocked greeting");
verify(this.exampleService, times(1)).greeting();
verifyNoMoreInteractions(this.exampleService);
}
}
@@ -0,0 +1,207 @@
/*
* Copyright 2002-present 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.constructor;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.test.context.bean.override.example.CustomQualifier;
import org.springframework.test.context.bean.override.example.ExampleService;
import org.springframework.test.context.bean.override.example.RealExampleService;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.test.mockito.MockitoAssertions;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.springframework.test.mockito.MockitoAssertions.assertIsMock;
/**
* Integration tests for {@link MockitoBean @MockitoBean} that use by-type lookup
* on constructor parameters.
*
* @author Sam Brannen
* @since 7.1
* @see <a href="https://github.com/spring-projects/spring-framework/issues/36096">gh-36096</a>
* @see org.springframework.test.context.bean.override.mockito.MockitoBeanByTypeLookupIntegrationTests
*/
@SpringJUnitConfig
class MockitoBeanByTypeLookupForConstructorParametersIntegrationTests {
final AnotherService serviceIsNotABean;
final ExampleService anyNameForService;
final StringBuilder ambiguous;
final StringBuilder ambiguousMeta;
MockitoBeanByTypeLookupForConstructorParametersIntegrationTests(
@MockitoBean AnotherService serviceIsNotABean,
@MockitoBean ExampleService anyNameForService,
@MockitoBean @Qualifier("prefer") StringBuilder ambiguous,
@MockitoBean @CustomQualifier StringBuilder ambiguousMeta) {
this.serviceIsNotABean = serviceIsNotABean;
this.anyNameForService = anyNameForService;
this.ambiguous = ambiguous;
this.ambiguousMeta = ambiguousMeta;
}
@Test
void mockIsCreatedWhenNoCandidateIsFound() {
assertIsMock(this.serviceIsNotABean);
when(this.serviceIsNotABean.hello()).thenReturn("Mocked hello");
assertThat(this.serviceIsNotABean.hello()).isEqualTo("Mocked hello");
verify(this.serviceIsNotABean, times(1)).hello();
verifyNoMoreInteractions(this.serviceIsNotABean);
}
@Test
void overrideIsFoundByType(ApplicationContext ctx) {
assertThat(this.anyNameForService)
.satisfies(MockitoAssertions::assertIsMock)
.isSameAs(ctx.getBean("example"))
.isSameAs(ctx.getBean(ExampleService.class));
when(this.anyNameForService.greeting()).thenReturn("Mocked greeting");
assertThat(this.anyNameForService.greeting()).isEqualTo("Mocked greeting");
verify(this.anyNameForService, times(1)).greeting();
verifyNoMoreInteractions(this.anyNameForService);
}
@Test
void overrideIsFoundByTypeAndDisambiguatedByQualifier(ApplicationContext ctx) {
assertThat(this.ambiguous)
.satisfies(MockitoAssertions::assertIsMock)
.isSameAs(ctx.getBean("ambiguous2"));
assertThatExceptionOfType(NoUniqueBeanDefinitionException.class)
.isThrownBy(() -> ctx.getBean(StringBuilder.class))
.satisfies(ex -> assertThat(ex.getBeanNamesFound()).containsOnly("ambiguous1", "ambiguous2"));
assertThat(this.ambiguous).isEmpty();
assertThat(this.ambiguous.substring(0)).isNull();
verify(this.ambiguous, times(1)).length();
verify(this.ambiguous, times(1)).substring(anyInt());
verifyNoMoreInteractions(this.ambiguous);
}
@Test
void overrideIsFoundByTypeAndDisambiguatedByMetaQualifier(ApplicationContext ctx) {
assertThat(this.ambiguousMeta)
.satisfies(MockitoAssertions::assertIsMock)
.isSameAs(ctx.getBean("ambiguous1"));
assertThatExceptionOfType(NoUniqueBeanDefinitionException.class)
.isThrownBy(() -> ctx.getBean(StringBuilder.class))
.satisfies(ex -> assertThat(ex.getBeanNamesFound()).containsOnly("ambiguous1", "ambiguous2"));
assertThat(this.ambiguousMeta).isEmpty();
assertThat(this.ambiguousMeta.substring(0)).isNull();
verify(this.ambiguousMeta, times(1)).length();
verify(this.ambiguousMeta, times(1)).substring(anyInt());
verifyNoMoreInteractions(this.ambiguousMeta);
}
@Nested
class NestedTests {
@Autowired
ExampleService localAnyNameForService;
final NestedService nestedService;
NestedTests(@MockitoBean NestedService nestedService) {
this.nestedService = nestedService;
}
@Test
void mockFromEnclosingClassConstructorParameterIsAccessibleViaAutowiring(ApplicationContext ctx) {
assertThat(this.localAnyNameForService)
.satisfies(MockitoAssertions::assertIsMock)
.isSameAs(anyNameForService)
.isSameAs(ctx.getBean("example"))
.isSameAs(ctx.getBean(ExampleService.class));
}
@Test
void nestedConstructorParameterIsAMock() {
assertIsMock(this.nestedService);
when(this.nestedService.hello()).thenReturn("Nested hello");
assertThat(this.nestedService.hello()).isEqualTo("Nested hello");
verify(this.nestedService).hello();
verifyNoMoreInteractions(this.nestedService);
}
}
public interface AnotherService {
String hello();
}
public interface NestedService {
String hello();
}
@Configuration(proxyBeanMethods = false)
static class Config {
@Bean("example")
ExampleService bean1() {
return new RealExampleService("Production hello");
}
@Bean("ambiguous1")
@Order(1)
@CustomQualifier
StringBuilder bean2() {
return new StringBuilder("bean2");
}
@Bean("ambiguous2")
@Order(2)
@Qualifier("prefer")
StringBuilder bean3() {
return new StringBuilder("bean3");
}
}
}
@@ -0,0 +1,165 @@
/*
* Copyright 2002-present 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.constructor;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
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.example.RealExampleService;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.test.mockito.MockitoAssertions;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
/**
* Integration tests for {@link MockitoSpyBean @MockitoSpyBean} that use by-name
* lookup on constructor parameters.
*
* @author Sam Brannen
* @since 7.1
* @see <a href="https://github.com/spring-projects/spring-framework/issues/36096">gh-36096</a>
* @see org.springframework.test.context.bean.override.mockito.MockitoSpyBeanByNameLookupTestMethodScopedExtensionContextIntegrationTests
*/
@SpringJUnitConfig
class MockitoSpyBeanByNameLookupForConstructorParametersIntegrationTests {
final ExampleService service1;
final ExampleService service2;
final ExampleService service3;
MockitoSpyBeanByNameLookupForConstructorParametersIntegrationTests(
@MockitoSpyBean ExampleService s1,
@MockitoSpyBean("s2") ExampleService service2,
@MockitoSpyBean @Qualifier("s3") ExampleService service3) {
this.service1 = s1;
this.service2 = service2;
this.service3 = service3;
}
@Test
void parameterNameIsUsedAsBeanName(ApplicationContext ctx) {
assertThat(this.service1)
.satisfies(MockitoAssertions::assertIsSpy)
.isSameAs(ctx.getBean("s1"));
assertThat(this.service1.greeting()).isEqualTo("prod 1");
verify(this.service1).greeting();
verifyNoMoreInteractions(this.service1);
}
@Test
void explicitBeanNameOverridesParameterName(ApplicationContext ctx) {
assertThat(this.service2)
.satisfies(MockitoAssertions::assertIsSpy)
.isSameAs(ctx.getBean("s2"));
assertThat(this.service2.greeting()).isEqualTo("prod 2");
verify(this.service2).greeting();
verifyNoMoreInteractions(this.service2);
}
@Test
void qualifierIsUsedToResolveByName(ApplicationContext ctx) {
assertThat(this.service3)
.satisfies(MockitoAssertions::assertIsSpy)
.isSameAs(ctx.getBean("s3"));
assertThat(this.service3.greeting()).isEqualTo("prod 3");
verify(this.service3).greeting();
verifyNoMoreInteractions(this.service3);
}
@Nested
class NestedTests {
@Autowired
@Qualifier("s1")
ExampleService localService1;
final ExampleService nestedSpy;
NestedTests(@MockitoSpyBean("s4") ExampleService nestedSpy) {
this.nestedSpy = nestedSpy;
}
@Test
void spyFromEnclosingClassIsAccessibleViaAutowiring(ApplicationContext ctx) {
assertThat(this.localService1)
.satisfies(MockitoAssertions::assertIsSpy)
.isSameAs(service1)
.isSameAs(ctx.getBean("s1"));
assertThat(this.localService1.greeting()).isEqualTo("prod 1");
verify(this.localService1).greeting();
verifyNoMoreInteractions(this.localService1);
}
@Test
void nestedConstructorParameterIsASpy(ApplicationContext ctx) {
assertThat(this.nestedSpy)
.satisfies(MockitoAssertions::assertIsSpy)
.isSameAs(ctx.getBean("s4"));
assertThat(this.nestedSpy.greeting()).isEqualTo("prod 4");
verify(this.nestedSpy).greeting();
verifyNoMoreInteractions(this.nestedSpy);
}
}
@Configuration(proxyBeanMethods = false)
static class Config {
@Bean
ExampleService s1() {
return new RealExampleService("prod 1");
}
@Bean
ExampleService s2() {
return new RealExampleService("prod 2");
}
@Bean
ExampleService s3() {
return new RealExampleService("prod 3");
}
@Bean
ExampleService s4() {
return new RealExampleService("prod 4");
}
}
}
@@ -0,0 +1,213 @@
/*
* Copyright 2002-present 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.constructor;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.test.context.bean.override.example.CustomQualifier;
import org.springframework.test.context.bean.override.example.ExampleService;
import org.springframework.test.context.bean.override.example.RealExampleService;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.test.mockito.MockitoAssertions;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatException;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
/**
* Integration tests for {@link MockitoSpyBean @MockitoSpyBean} that use by-type
* lookup on constructor parameters.
*
* @author Sam Brannen
* @since 7.1
* @see <a href="https://github.com/spring-projects/spring-framework/issues/36096">gh-36096</a>
* @see org.springframework.test.context.bean.override.mockito.MockitoSpyBeanByTypeLookupIntegrationTests
*/
@SpringJUnitConfig
class MockitoSpyBeanByTypeLookupForConstructorParametersIntegrationTests {
final ExampleService anyNameForService;
final StringHolder ambiguous;
final StringHolder ambiguousMeta;
MockitoSpyBeanByTypeLookupForConstructorParametersIntegrationTests(
@MockitoSpyBean ExampleService anyNameForService,
@MockitoSpyBean @Qualifier("prefer") StringHolder ambiguous,
@MockitoSpyBean @CustomQualifier StringHolder ambiguousMeta) {
this.anyNameForService = anyNameForService;
this.ambiguous = ambiguous;
this.ambiguousMeta = ambiguousMeta;
}
@Test
void overrideIsFoundByType(ApplicationContext ctx) {
assertThat(this.anyNameForService)
.satisfies(MockitoAssertions::assertIsSpy)
.isSameAs(ctx.getBean("example"))
.isSameAs(ctx.getBean(ExampleService.class));
assertThat(this.anyNameForService.greeting()).isEqualTo("Production hello");
verify(this.anyNameForService).greeting();
verifyNoMoreInteractions(this.anyNameForService);
}
@Test
void overrideIsFoundByTypeAndDisambiguatedByQualifier(ApplicationContext ctx) {
assertThat(this.ambiguous)
.satisfies(MockitoAssertions::assertIsSpy)
.isSameAs(ctx.getBean("ambiguous2"));
assertThatException()
.isThrownBy(() -> ctx.getBean(StringHolder.class))
.withMessageEndingWith("but found 2: ambiguous1,ambiguous2");
assertThat(this.ambiguous.getValue()).isEqualTo("bean3");
assertThat(this.ambiguous.size()).isEqualTo(5);
verify(this.ambiguous).getValue();
verify(this.ambiguous).size();
verifyNoMoreInteractions(this.ambiguous);
}
@Test
void overrideIsFoundByTypeAndDisambiguatedByMetaQualifier(ApplicationContext ctx) {
assertThat(this.ambiguousMeta)
.satisfies(MockitoAssertions::assertIsSpy)
.isSameAs(ctx.getBean("ambiguous1"));
assertThatException()
.isThrownBy(() -> ctx.getBean(StringHolder.class))
.withMessageEndingWith("but found 2: ambiguous1,ambiguous2");
assertThat(this.ambiguousMeta.getValue()).isEqualTo("bean2");
assertThat(this.ambiguousMeta.size()).isEqualTo(5);
verify(this.ambiguousMeta).getValue();
verify(this.ambiguousMeta).size();
verifyNoMoreInteractions(this.ambiguousMeta);
}
@Nested
class NestedTests {
@Autowired
ExampleService localAnyNameForService;
final AnotherService nestedSpy;
NestedTests(@MockitoSpyBean AnotherService nestedSpy) {
this.nestedSpy = nestedSpy;
}
@Test
void spyFromEnclosingClassConstructorParameterIsAccessibleViaAutowiring(ApplicationContext ctx) {
assertThat(this.localAnyNameForService)
.satisfies(MockitoAssertions::assertIsSpy)
.isSameAs(anyNameForService)
.isSameAs(ctx.getBean("example"))
.isSameAs(ctx.getBean(ExampleService.class));
assertThat(this.localAnyNameForService.greeting()).isEqualTo("Production hello");
verify(this.localAnyNameForService).greeting();
verifyNoMoreInteractions(this.localAnyNameForService);
}
@Test
void nestedConstructorParameterIsASpy(ApplicationContext ctx) {
assertThat(this.nestedSpy)
.satisfies(MockitoAssertions::assertIsSpy)
.isSameAs(ctx.getBean("anotherService"))
.isSameAs(ctx.getBean(AnotherService.class));
assertThat(this.nestedSpy.hello()).isEqualTo("Another hello");
verify(this.nestedSpy).hello();
verifyNoMoreInteractions(this.nestedSpy);
}
}
interface AnotherService {
String hello();
}
static class StringHolder {
private final String value;
StringHolder(String value) {
this.value = value;
}
public String getValue() {
return this.value;
}
public int size() {
return this.value.length();
}
}
@Configuration(proxyBeanMethods = false)
static class Config {
@Bean("example")
ExampleService bean1() {
return new RealExampleService("Production hello");
}
@Bean("ambiguous1")
@Order(1)
@CustomQualifier
StringHolder bean2() {
return new StringHolder("bean2");
}
@Bean("ambiguous2")
@Order(2)
@Qualifier("prefer")
StringHolder bean3() {
return new StringHolder("bean3");
}
@Bean
AnotherService anotherService() {
return new AnotherService() {
@Override
public String hello() {
return "Another hello";
}
};
}
}
}
@@ -0,0 +1,20 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.context.bean.override.mockito.typelevel;
interface ConstructorService01 extends Service {
}
@@ -48,6 +48,10 @@ import static org.springframework.test.mockito.MockitoAssertions.assertIsNotMock
@MockitoBean(name = "s2", types = ExampleService.class)
class MockitoBeansByNameIntegrationTests {
final ExampleService service0A;
final ExampleService service0B;
final ExampleService service0C;
@Autowired
ExampleService s1;
@@ -62,6 +66,16 @@ class MockitoBeansByNameIntegrationTests {
ExampleService service4;
MockitoBeansByNameIntegrationTests(@MockitoBean ExampleService s0A,
@MockitoBean(name = "s0B") ExampleService service0B,
@MockitoBean @Qualifier("s0C") ExampleService service0C) {
this.service0A = s0A;
this.service0B = service0B;
this.service0C = service0C;
}
@BeforeEach
void configureMocks() {
assertIsMock(s1, "s1");
@@ -86,6 +100,21 @@ class MockitoBeansByNameIntegrationTests {
@Configuration
static class Config {
@Bean
ExampleService s0A() {
return () -> "prod 0A";
}
@Bean
ExampleService s0B() {
return () -> "prod 0B";
}
@Bean
ExampleService s0C() {
return () -> "prod 0C";
}
@Bean
ExampleService s1() {
return () -> "prod 1";
@@ -63,10 +63,17 @@ class MockitoBeansByTypeIntegrationTests implements MockTestInterface01 {
@Autowired
Service06 service06;
final ConstructorService01 constructorService01;
@MockitoBean
Service07 service07;
MockitoBeansByTypeIntegrationTests(@MockitoBean ConstructorService01 constructorService01) {
this.constructorService01 = constructorService01;
}
@BeforeEach
void configureMocks() {
assertIsMock(service01, "service01");
@@ -75,6 +82,7 @@ class MockitoBeansByTypeIntegrationTests implements MockTestInterface01 {
assertIsMock(service04, "service04");
assertIsMock(service05, "service05");
assertIsMock(service06, "service06");
assertIsMock(constructorService01, "constructorService01");
assertIsMock(service07, "service07");
given(service01.greeting()).willReturn("mock 01");
@@ -83,6 +91,7 @@ class MockitoBeansByTypeIntegrationTests implements MockTestInterface01 {
given(service04.greeting()).willReturn("mock 04");
given(service05.greeting()).willReturn("mock 05");
given(service06.greeting()).willReturn("mock 06");
given(constructorService01.greeting()).willReturn("mock constructor 01");
given(service07.greeting()).willReturn("mock 07");
}
@@ -94,6 +103,7 @@ class MockitoBeansByTypeIntegrationTests implements MockTestInterface01 {
assertThat(service04.greeting()).isEqualTo("mock 04");
assertThat(service05.greeting()).isEqualTo("mock 05");
assertThat(service06.greeting()).isEqualTo("mock 06");
assertThat(constructorService01.greeting()).isEqualTo("mock constructor 01");
assertThat(service07.greeting()).isEqualTo("mock 07");
}
@@ -133,6 +143,7 @@ class MockitoBeansByTypeIntegrationTests implements MockTestInterface01 {
assertIsMock(service04, "service04");
assertIsMock(service05, "service05");
assertIsMock(service06, "service06");
assertIsMock(constructorService01, "constructorService01");
assertIsMock(service07, "service07");
assertIsMock(service08, "service08");
assertIsMock(service09, "service09");
@@ -157,6 +168,7 @@ class MockitoBeansByTypeIntegrationTests implements MockTestInterface01 {
assertThat(service04.greeting()).isEqualTo("mock 04");
assertThat(service05.greeting()).isEqualTo("mock 05");
assertThat(service06.greeting()).isEqualTo("mock 06");
assertThat(constructorService01.greeting()).isEqualTo("mock constructor 01");
assertThat(service07.greeting()).isEqualTo("mock 07");
assertThat(service08.greeting()).isEqualTo("mock 08");
assertThat(service09.greeting()).isEqualTo("mock 09");
@@ -44,7 +44,7 @@ class MockitoBeansTests {
Stream<Class<?>> mockedServices = getRegisteredMockTypes(MockitoBeansByTypeIntegrationTests.class);
assertThat(mockedServices).containsExactly(
Service01.class, Service02.class, Service03.class, Service04.class,
Service05.class, Service06.class, Service07.class);
Service05.class, Service06.class, ConstructorService01.class, Service07.class);
}
@Test
@@ -52,8 +52,8 @@ class MockitoBeansTests {
Stream<Class<?>> 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,
Service05.class, Service06.class, ConstructorService01.class, Service07.class,
Service08.class, Service09.class, Service10.class, Service11.class, Service12.class,
Service13.class);
}