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() {
// ...
}
Comment From: OrangeDog
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();
}
Comment From: jack5505
Can I take this issue to work on this
Comment From: sbrannen
Hi @jack5505,
The team has not yet made a decision regarding this issue, and it's highly unlikely that the implementation would be performed by anyone outside the core team.
Thanks for the offer anyway.
Comment From: sbrannen
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 implicitvalue
attribute since@MockitoBean
already has avalue
attribute which is an alias for thename
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
Comment From: cfredri4
The composed annotation example would solve my cases.
We're not using @SpyBean
on a test class.
Comment From: nmck257
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
Comment From: Saljack
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)
Comment From: JanMosigItemis
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.
Comment From: sergerot
Another shorter annotation variant could be something like:
@MockitoBean(type = {FirstService.class, SecondService.class})
class MyIntegrationTests {
}
Comment From: emouty
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 https://github.com/spring-projects/spring-framework/issues/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())
//...
}
Comment From: daniel-frak
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.
Comment From: sbrannen
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
Comment From: sbrannen
Update: current work on this can be viewed in the following feature branch.
- https://github.com/spring-projects/spring-framework/compare/main...sbrannen:spring-framework:issues/gh-33925-MockitoBeans
MockitoBeansByTypeIntegrationTests
and MockitoBeansByNameIntegrationTests
demonstrate most supported use cases for @MockitoBean
at the class/interface level.
Comment From: sbrannen
This new feature was released today with Spring Framework 6.2.2.
Please check out the updated documentation for details and examples!
Comment From: boris-faniuk-n26
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.
Comment From: sbrannen
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
Comment From: OrangeDog
Just to note that unlike @MockBean
, @MockitoBean
only works on test classes, not configuration classes.
Comment From: dominioon
Just to note that unlike
@MockBean
,@MockitoBean
only works on test classes, not configuration classes.
Are there plans to fix it?
Comment From: tobias-lippert
No there are not, see #33934