@MockInBean and @SpyInBean are alternatives to @MockBean and @SpyBean for Spring Boot tests (>= 2.2.0 including >= 3.X.X).
They surgically replace a field value in a Spring Bean by a Mock/Spy for the duration of a test and set back the original value afterwards, leaving the Spring Context clean.
@MockInBean 'mocks a bean in a bean' whereas @MockBean 'mocks a bean in the whole context'.
Spring Context pollution was a fairly common problem before the introduction of @MockBean and @SpyBean. Many developers would inject mocks in beans during tests through @InjectMock
or using manual setters and often forget to set back the original field values which would leave the Spring context polluted and cause test failures in unrelated tests.
@MockBean and @SpyBean solved this issue by providing Mockito injection directly in the Spring Context but introduced an undesirable side-effect: their usage dirties the context and may lead to the re-creation of new Spring contexts for any unique combination, which can be incredibly time-consuming. See The Problems with @MockBean
Assuming you really need to run the test in the Spring Context, the most straight-forward solution is still to inject your mock/spy in your bean and reset it afterwards.
@MockInBean and @SpyInBean brings the convenience of @MockBean and @SpyBean and do just that:
- Set the mock/spy in the bean.
- Replace the mock/spy by the original value afterwards.
Assuming that you want to test the following service:
@Service
public class MyService {
@Autowired
protected ThirdPartyApiService thirdPartyService;
@Autowired
protected ExpensiveProcessor expensiveProcessor;
public void doSomething() {
final Object somethingExpensive = expensiveProcessor.returnSomethingExpensive();
thirdPartyService.doSomethingOnThirdPartyApi(somethingExpensive);
}
}
You can write your test this way:
@SpringBootTest
public class MyServiceTest {
@MockInBean(MyService.class)
private ThirdPartyApiService thirdPartyApiService;
@SpyInBean(MyService.class)
private ExpensiveProcessor expensiveProcessor;
@Autowired
private MyService myService;
@Test
public void test() {
final Object somethingExpensive = new Object();
Mockito.when(expensiveProcessor.returnSomethingExpensive()).thenReturn(somethingExpensive);
myService.doSomething();
Mockito.verify(thirdPartyApiService).doSomethingOnThirdPartyApi(somethingExpensive);
}
}
What happens:
Before each test:
MyServiceTest.thirdPartyService
will be created as a mock and injected in the target of @MockInBean:MyService
.MyServiceTest.expensiveProcessor
will be created as a spy of the beanexpensiveProcessor
and injected in the target of @SpyInBean:MyService
.
After the tests of MyServiceTest
are done:
MyService.thirdPartyService
will be reset to the original Spring beanthirdPartyService
MyService.expensiveProcessor
will be reset to the original Spring beanexpensiveProcessor
Simply include the maven dependency (from central maven) to start using @MockInBean and @SpyInBean in your tests.
<dependency>
<groupId>com.teketik</groupId>
<artifactId>mock-in-bean</artifactId>
<version>boot2-v1.7</version>
<scope>test</scope>
</dependency>
@MockInBean and @SpyInBean also support:
- Injection in multiple Spring beans: Repeat the annotation on your field.
- Injection in bean identified by name if multiple instances exist in the context: Specify a
name
in your annotation.
Checkout the javadoc for more information.
This approach has some limitations compared to the @MockBean/@SpyBean equivalent:
- There is currently no isolation per thread. It is not advised to use this library in parallel test suites. (Parallel test execution is supported experimentally in https://github.com/antoinemeyer/mock-in-bean/tree/v2.1-RC).
- Operations on beans happening within another bean's constructor will not be performed against the mocks since the mocks are injected directly into the fields. Do not use @MockInBean/@SpyInBean for beans that are manipulated in constructors.