Skip to content

Support @MockitoBean at the type level on test classes #33925

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
nmck257 opened this issue Nov 20, 2024 · 21 comments
Closed

Support @MockitoBean at the type level on test classes #33925

nmck257 opened this issue Nov 20, 2024 · 21 comments
Assignees
Labels
in: test Issues in the test module type: enhancement A general enhancement
Milestone

Comments

@nmck257
Copy link

nmck257 commented Nov 20, 2024

MockitoBean is designed to replace the now-deprecated MockBean annotation. MockBean could target either a field or a type. Currently, MockitoBean can only target a field. This issue proposes adding the support to target types to MockitoBean, similar to MockBean.

(pasting some prose from #29917 comments)

This feature is useful for test cases which want to stub out some set of beans (ie to avoid some side effect of regular instantiation), but didn't care about specifying behavior.

Suppose you have multiple @SpringBootTest classes, and want to stub out the same set of beans for each of them. With an annotation targeting the class itself, you could define a meta-annotation containing those repeated invocations of MockBean and reuse wherever needed. If we only have support for mocking beans as fields, then the next alternative would be a common superclass which declares those annotated fields. That pattern pushes you to (single) inheritance, whereas the class annotation pattern was composition-friendly.

Subjectively, I also think that if the test developer's intent is not to define any behavior for the mocked bean, then it's easier to read and maintain if we can avoid adding an unused field to the class scope.

Here's some Kotlin code to help demonstrate the point above:

@MockBeans(
    MockBean(MyServiceWithOutOfProcessSideEffectsOnBoot::class),
    MockBean(MyPreemptiveClientAuthTokenFetcher::class),
    MockBean(MyServiceWhichLoadsOneMillionThingsIntoMemoryOnBoot::class),
)
annotation class MockExpensiveDependencies

@SpringBootTest
@MockExpensiveDependencies
class BusinessLogicControllerTest {
    // ...
}

@SpringBootTest
@MockExpensiveDependencies
class SomeOtherTest {
    // ...
}

vs

open class MockExpensiveDependenciesBase {
    @MockBean
    private lateinit var foo: MyServiceWithOutOfProcessSideEffectsOnBoot
    @MockBean
    private lateinit var bar: MyPreemptiveClientAuthTokenFetcher
    @MockBean
    private lateinit var gav: MyServiceWhichLoadsOneMillionThingsIntoMemoryOnBoot
}

@SpringBootTest
class BusinessLogicControllerTest : MockExpensiveDependenciesBase() {
    // ...
}

@SpringBootTest
class SomeOtherTest : MockExpensiveDependenciesBase() {
    // ...
}
@OrangeDog
Copy link

Here's a couple of other cases that @MockBean allowed.

Including mocks via additional config (not just other annotations):

@TestConfiguration
@MockBean({
    MyServiceWithOutOfProcessSideEffectsOnBoot.class,
    MyPreemptiveClientAuthTokenFetcher.class
})
@EnableSomething
@Import(OtherStuff.class)
public class SharedTestConfiguration { }

@SpringTestAnnotation
@Import({SharedTestConfiguration.class, SomeOtherConfig.class})
public class SomeTests {

If one of these mocks were autowired, stub behaviour could then be specified.
This doesn't work when returning a mock from a @Bean method, depending on how beans are proxied:

@SpringTestAnnotation
@MockBean(MyServiceWithOutOfProcessSideEffectsOnBoot.class)  // actually declared elsewhere
public class SomeTests {
    @Autowired private MyServiceWithOutOfProcessSideEffectsOnBoot myService;

    @Test
    public void onlyThisTestSpecificallyWantsToStubIt() {
        doReturn(true).when(myService).isSomething();
    }

@sbrannen sbrannen added the in: test Issues in the test module label Nov 21, 2024
@sbrannen sbrannen changed the title MockitoBean annotation -- add support to target types/classes Provide class-level @MockitoBean support Nov 21, 2024
@jack5505

This comment was marked as outdated.

@sbrannen

This comment was marked as outdated.

@sbrannen sbrannen changed the title Provide class-level @MockitoBean support Support @MockitoBean at the class level on test classes Nov 24, 2024
@sbrannen
Copy link
Member

sbrannen commented Dec 2, 2024

We appreciate the constructive feedback we have received here as well as in #33934, and in light of that we are considering introducing support for declaring @MockitoBean at the class level on test classes, with the driving impetus being the ability to reuse mock configuration across a test suite without having to extend a base test class.

If we choose to do so, that would allow @MockitoBean to be declared directly (or as a meta-annotation) on a test class (or on any superclass or interface in the type hierarchy of the test class). However, that does not mean that @MockitoBean would be supported on a @Configuration class.

In terms of declaring multiple types to mock (with different mock configurations), that could be supported by allowing @MockitoBean to be used as a repeatable annotation or introducing a dedicated, required container annotation such as @MockitoBeans.

In any case, @MockitoBean would need a new attribute to allow the developer to specify which type or types to mock, since the type could no longer be inferred from the annotated field's type.

The following demonstrates what such a programming model might look like.

@SpringJUnitConfig
// Register one or more types to mock
@MockitoBean(types = { ExampleService.class, AnotherService.class })
class MyIntegrationTests {

	// Optionally inject the mocks for stubbing and verification.
	//
	// @Autowired
	// ExampleService exampleService;
	//
	// @Autowired
	// AnotherService anotherService;

	@Test
	void test() {
		// ...
	}
}

For reuse of the mock configuration across the test suite, teams could introduce composed annotations such as the following.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@MockitoBean(types = { ExampleService.class, AnotherService.class })
public @interface SharedMocks {
}

That would allow @SharedMocks to be declared on any test class (or on any superclass or interface in the type hierarchy of the test class) as follows.

@SpringJUnitConfig
// Reuse @MockitoBean declarations via a composed annotation:
@SharedMocks
class MyIntegrationTests {

	// Optionally inject the mocks for stubbing and verification.
	//
	// @Autowired
	// ExampleService exampleService;
	//
	// @Autowired
	// AnotherService anotherService;

	@Test
	void test() {
		// ...
	}
}

Another option would be to limit one type per @MockitoBean annotation, such as in the following example, where type can only supply a single Class reference to mock.

@SpringJUnitConfig
@MockitoBean(type = ExampleService.class)
@MockitoBean(type = AnotherService.class)
class MyIntegrationTests {

	// Optionally inject the mocks for stubbing and verification.
	//
	// @Autowired
	// ExampleService exampleService;
	//
	// @Autowired
	// AnotherService anotherService;

	@Test
	void test() {
		// ...
	}
}

NOTE: In contrast to Spring Boot's @MockBean annotation, the type or types to mock cannot be specified via an explicit or implicit value attribute since @MockitoBean already has a value attribute which is an alias for the name attribute.


As I mentioned previously, we are not yet committed to implementing this feature, but we are considering it.

Thus, if you are interested in this GitHub issue or #33934, we would appreciate your feedback on whether the above proposal would satisfy your needs to reuse @MockitoBean across your test suite.

As a side note, we would also be interested in knowing how many Spring Boot users have traditionally declared @SpyBean on a test class as opposed to on a field in a test class.

Thanks,

Sam

@sbrannen sbrannen changed the title Support @MockitoBean at the class level on test classes Support @MockitoBean at the type level on test classes Dec 2, 2024
@cfredri4
Copy link
Contributor

cfredri4 commented Dec 2, 2024

The composed annotation example would solve my cases.
We're not using @SpyBeanon a test class.

@nmck257
Copy link
Author

nmck257 commented Dec 2, 2024

Thanks for the update @sbrannen !

Here are some responses to topics raised in your comment:

  • Targeting only test classes (and not Configuration classes) is suitable for my usage
  • No preference on repeatable vs container annotations; I think I recall that the container pattern predates JDK support for repeatable annotations, and the repeatable pattern may be a bit simpler?
  • I have not used @SpyBean on a test class; my intuition is that I would only use SpyBean for an element which I expect to reference directly in my test cases, and if I expect to reference a (stubbed) bean in my test cases, then declaring it as a field is less code anyway compared to declaring the stubbing at the class level and autowiring the resultant bean into a field
  • Adding a type field makes sense, though it prompts a design decision about how to handle contexts which declare multiple beans of that same type. For the old MockBean, the name was required in that case, but it may also be useful to mock all beans of that type if the name is not specified

@Saljack
Copy link

Saljack commented Dec 3, 2024

Maybe it's a stupid idea, but isn't it better to create a new annotation for it? Because I feel it is misuse for @MockitoBean. If it was originally designed differently than @MockBean why not introduce a new annotation? That can be used also for spy bean with an attribute (I do not use it on classes). People will need to change their code anyway so it does not matter if there are two annotions one for field and one for classes. For example:

@MockitoBeanClass(value=ExampleService.class, spy=SpiedService.class)
@MockitoBeanClass(OtherExampleService.class)

@JanMosigItemis
Copy link

JanMosigItemis commented Dec 3, 2024

Hi and many thanks for providing an open discussion on this topic 👍

In my current project, we used to heavily rely on @MockBean and @MockBeans at the class level. That helped us to create custom test annotations which helped to reduce "white mocking" noise.

For example:

@SpringBootTest
@Import({...configs...})
@MockBeans({@MockBean(ClassA.class), @MockBean(ClassB.class)})
public @interface BookingTest {}
@BookingTest
class BookingPaymentIT {

  @Test
  void testSomething() {
    // ...
  }
}

As opposed to:

@SpringBootTest
@Import({...configs...})
class BookingPaymentIT {

  @MockitoBean
  private ClassA classAMock;

  @MockitoBean
  private ClassB classBMock;

  @Test
  void testSomething() {
    // ...
  }
}

Also this reduced redundancy, because we could reuse the mock "definitions".

Although with Spring Boot 3.4.x this may also be achieved in a different way, we found the old way to be very convenient and straightforward.

@sergerot
Copy link

sergerot commented Dec 3, 2024

Another shorter annotation variant could be something like:

@MockitoBean(type = {FirstService.class, SecondService.class})
class MyIntegrationTests {
}

@sbrannen sbrannen self-assigned this Dec 3, 2024
@emouty
Copy link

emouty commented Dec 3, 2024

On my projects, we heavily rely on @MockBean for our ITs. Our standard way of doing things currently is
having a @Component class per @MockBean with helpers in it. Then we have a class that implements BeforeEachCallback AfterEachCallback in which we get the mocked bean from the Spring context in order to initialize mocked responses that are needed every time. A Simplified example would be something like

@Component
public class ClientAMockBean {
    @MockBean
    private ClientA clientA;

    public void stubInitCatalog(){
        when(clientA).getCatalog()).thenReturn(initCatalog()));
    }
    public void stubGetLiveProduct(){
        when(clientA).getProduct(any()).thenReturn(new Product(UUID.randomUUID()));
    }
    public void stubPurchaseProduct(UUID id){
        when(clientA).buyProduct(eq(id)).thenReturn(new Transaction(UUID.randomUUID(),id));
    }
    //...
}
public class ITCommonCallbacks implements BeforeEachCallback, BeforeAllCallback{

    private ClientAMockBean clientAMockBean;

    @Override
    public void beforeAll(ExtensionContext context) {
        ApplicationContext springContext = SpringExtension.getApplicationContext(context);
        clientAMockBean = springContext.getBean(ClientAMockBean.class);
    }
    @Override
    public void beforeEach(ExtensionContext extensionContext) {
        clientAMockBean.stubInitCatalog();
    }
}
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ExtendWith({ ITCommonCallbacks.class })
class ProductPurchaseIT {
    @Autowired
    private ClientAMockBean clientAMockBean;
    @Autowired
    AppServer server;
    @Test
    void shouldBuyProduct(){
        var user = server.initUserCases();
        var productA = user.getFirstCatalogProduct();
        clientAMockBean.stubPurchaseProduct(productA.id())
        //...
    }

Ideally I would have liked to be able to have @MockitoBean on @Configuration but that's not planned based on #33934 . @MockitoBean at the type level could fit because I could group all my mocks into a single @SharedMocks annotation, thus easily reuse them across ITs and then I could inject them in the mock helper class.

@TestComponent("clientAMockBean")
public class ClientAMockBean {
    @Autowired
    private ClientA clientA;

    public void stubInitCatalog(){
        when(clientA).getCatalog()).thenReturn(initCatalog()));
    }
    public void stubGetLiveProduct(){
        when(clientA).getProduct(any()).thenReturn(new Product(UUID.randomUUID()));
    }
    public void stubPurchaseProduct(UUID id){
        when(clientA).buyProduct(eq(id)).thenReturn(new Transaction(UUID.randomUUID(),id));
    }
    //...
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@MockitoBean(types = { ClientA.class, AnotherService.class })
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ExtendWith({ ITCommonCallbacks.class })
public @interface SpringBootIT {
}
@SpringBootIT
class ProductPurchaseIT {
    @Autowired
    private ClientAMockBean clientAMockBean;
    @Autowired
    AppServer server;
    @Test
    void shouldBuyProduct(){
        var user = server.initUserCases();
        var productA = user.getFirstCatalogProduct();
        clientAMockBean.stubPurchaseProduct(productA.id())
        //...
    }

@sbrannen sbrannen added type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Dec 3, 2024
@sbrannen sbrannen added this to the 6.2.x milestone Dec 3, 2024
@sbrannen sbrannen added the status: pending-design-work Needs design work before any code can be developed label Dec 3, 2024
@daniel-frak
Copy link

daniel-frak commented Dec 13, 2024

Chiming in to say that the ability to declare @MockitoBean as part of a meta-annotation on test classes would solve my use case, as well.

I currently have several annotations in my project, such as @ControllerTest, @JpaRepositoryTest and @WebSocketPublisherTest and each of these provides its own @MockBeans, so that the same Spring Contexts can be reused between controller tests, repository tests and so on.

I'm currently declaring @MockBean on @TestConfiguration classes, but I could just as easily define @MockitoBean on the meta-annotations themselves (if that was allowed).

I am eagerly hoping that you decide to support this.

@sbrannen
Copy link
Member

Hi everybody,

Thanks for all of the feedback!

The current plan is to support @MockitoBean as a repeatable annotation that can be declared on a test class (or one of its superclasses or implemented interfaces). This would naturally allow @MockitoBean to be used as a meta-annotation on a composed annotation declared at the test class level, which would allow the mock configuration to be reused across test classes via a single composed annotation.

This issue is currently assigned to the 6.2.x backlog and "pending design work", with the hope of getting this feature implemented in time for 6.2.2.

Regards,

Sam

sbrannen added a commit to sbrannen/spring-framework that referenced this issue Jan 2, 2025
sbrannen added a commit to sbrannen/spring-framework that referenced this issue Jan 2, 2025
sbrannen added a commit to sbrannen/spring-framework that referenced this issue Jan 3, 2025
@sbrannen sbrannen modified the milestones: 6.2.x, 6.2.2 Jan 5, 2025
sbrannen added a commit to sbrannen/spring-framework that referenced this issue Jan 5, 2025
sbrannen added a commit to sbrannen/spring-framework that referenced this issue Jan 7, 2025
sbrannen added a commit to sbrannen/spring-framework that referenced this issue Jan 10, 2025
@sbrannen
Copy link
Member

sbrannen commented Jan 10, 2025

Update: current work on this can be viewed in the following feature branch.

MockitoBeansByTypeIntegrationTests and MockitoBeansByNameIntegrationTests demonstrate most supported use cases for @MockitoBean at the class/interface level.

sbrannen added a commit to sbrannen/spring-framework that referenced this issue Jan 13, 2025
sbrannen added a commit to sbrannen/spring-framework that referenced this issue Jan 15, 2025
sbrannen added a commit to sbrannen/spring-framework that referenced this issue Jan 15, 2025
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 spring-projectsgh-33925
@sbrannen
Copy link
Member

This new feature was released today with Spring Framework 6.2.2.

Please check out the updated documentation for details and examples!

ivicac added a commit to bytechefhq/bytechef that referenced this issue Jan 18, 2025
@boris-faniuk-n26
Copy link

Hi, there!
Just came across this issue after I updated to Spring Boot 3.4.
I just wanted to mention that I do use SpyBean on class level.
I have a base test configuration where some beans are declared as SpyBean so that I can spy on them without re-declaring this on every next test class.
As I understand the requested feature was only added for MockitoBean, but I would appreciate if this was added for MockitoSpyBean as well.
mockk supports this, previous spring boot supported this, I think it is reasonable to keep this option.

kaladay added a commit to folio-org/spring-module-core that referenced this issue Feb 6, 2025
Address newly deprecated code issues.

Address ambiguous `any()` issue that now appears and breaks tests.

The `@MockBean` is no longer available and we must instead use `@MockitoBean`.
see: spring-projects/spring-framework#29917 (comment)
see: spring-projects/spring-framework#33925
see: https://docs.spring.io/spring-framework/reference/testing/annotations/integration-spring/annotation-mockitobean.html
sbrannen added a commit that referenced this issue Feb 8, 2025
Changes made to the Bean Override search algorithms in commit
9181cce resulted in a regression that caused tests to start failing
due to duplicate BeanOverrideHandlers under the following circumstances.

- An enclosing class (typically a top-level test class) declares a
  @⁠BeanOverride such as @⁠MockitoBean.

- An inner class is declared in that enclosing class.

- A @⁠Nested test class which extends that inner class is declared in
  the same enclosing class.

The reason for the duplicate detection is that the current search
algorithm visits the common enclosing class twice.

To address that, this commit revises the search algorithm in
BeanOverrideHandler so that enclosing classes are only visited once.

See gh-33925
Closes gh-34324
@sbrannen
Copy link
Member

Hi @boris-faniuk-n26,

I just wanted to mention that I do use SpyBean on class level.

Thanks for mentioning that.

I created #34408 for us to investigate the feasibility of supporting @MockitoSpyBean at the type level on test classes (though, like with this issue, not on @Configuration classes).

Regards,

Sam

@OrangeDog
Copy link

Just to note that unlike @MockBean, @MockitoBean only works on test classes, not configuration classes.

sbrannen added a commit to sbrannen/spring-framework that referenced this issue Feb 12, 2025
Prior to this commit, @⁠MockitoSpyBean could only be declared on fields
within test classes, which prevented developers from being able to
easily reuse spy configuration across a test suite.

With this commit, @⁠MockitoSpyBean is now supported at the type level
on test classes, their superclasses, and interfaces implemented by
those classes. @⁠MockitoSpyBean is also supported on enclosing classes
for @⁠Nested test classes, their superclasses, and interfaces
implemented by those classes, while honoring @⁠NestedTestConfiguration
semantics.

In addition, @⁠MockitoSpyBean:

- has a new `types` attribute that can be used to declare the type or
  types to spy when @⁠MockitoSpyBean is declared at the type level

- can be declared as a repeatable annotation at the type level

- can be declared as a meta-annotation on a custom composed annotation
  which can be reused across a test suite (see the @⁠SharedSpies
  example in the reference manual)

To support these new features, this commit also includes the following
changes.

- MockitoSpyBeanOverrideProcessor has been revised to support
  @⁠MockitoSpyBean at the type level.

- The "Bean Overriding in Tests" and "@⁠MockitoBean and
  @⁠MockitoSpyBean" sections of the reference manual have been fully
  revised.

See spring-projectsgh-34408
Closes spring-projectsgh-33925
sbrannen added a commit that referenced this issue Feb 12, 2025
Prior to this commit, @⁠MockitoSpyBean could only be declared on fields
within test classes, which prevented developers from being able to
easily reuse spy configuration across a test suite.

With this commit, @⁠MockitoSpyBean is now supported at the type level
on test classes, their superclasses, and interfaces implemented by
those classes. @⁠MockitoSpyBean is also supported on enclosing classes
for @⁠Nested test classes, their superclasses, and interfaces
implemented by those classes, while honoring @⁠NestedTestConfiguration
semantics.

In addition, @⁠MockitoSpyBean:

- has a new `types` attribute that can be used to declare the type or
  types to spy when @⁠MockitoSpyBean is declared at the type level

- can be declared as a repeatable annotation at the type level

- can be declared as a meta-annotation on a custom composed annotation
  which can be reused across a test suite (see the @⁠SharedSpies
  example in the reference manual)

To support these new features, this commit also includes the following
changes.

- MockitoSpyBeanOverrideProcessor has been revised to support
  @⁠MockitoSpyBean at the type level.

- The "Bean Overriding in Tests" and "@⁠MockitoBean and
  @⁠MockitoSpyBean" sections of the reference manual have been fully
  revised.

See gh-34408
Closes gh-33925
@dominioon
Copy link

Just to note that unlike @MockBean, @MockitoBean only works on test classes, not configuration classes.

Are there plans to fix it?

@tobias-lippert
Copy link

No there are not, see #33934

@valerioj
Copy link

hey guys, someone mentioned here spring-projects/spring-boot#43348 that this is fixed.

and on stackoverflow i found this way to implement MockitoBean on class level:

@MockitoBean(types={F.class,B.class})

@OrangeDog
Copy link

@valerioj someone mentioned here too...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: test Issues in the test module type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests