I upgrade spring boot from 1.5.6.RELEASE to 2.2.0.RELEASE, But lead to Mockito @InjectMocks
annotation can't work, then I debug, find the order of MockitoTestExecutionListener
and SpringBootDependencyInjectionTestExecutionListener
is changed. MockitoTestExecutionListener
is ahead of SpringBootDependencyInjectionTestExecutionListener
, when autowire spring bean with @Autowired
, the field which is not inited by spring, Mockito will find the field value is null
and throw Exception.
Comment From: wilkinsona
MockitoTestExecutionListener
has an order of 2050 and SpringBootDependencyInjectionTestExecutionListener
has an order of 2000. The lower the value the higher the precedence so SpringBootDependencyInjectionTestExecutionListener
should run before MockitoTestExecutionListener
.
If you would like us to spend some more time investigating, can please take the time to create a minimal example that reproduces the problem you have described above? You can share it with us in a separate repository on GitHub or by zipping it up and attaching it to this issue.
Comment From: jacky1193610322
thank you for your reply. MockitoTestExecutionListener
has an order 1950 in the code. code source
Comment From: wilkinsona
You're right, sorry. It was set to 2050 when we first ordered it in 2.0, but then changed to 1950 to fix https://github.com/spring-projects/spring-boot/issues/11903. I don't think we can change the order back without regressing that fix.
I'm not sure I fully understand the problem you've described. Can you please provide a small sample that reproduces it?
Comment From: jacky1193610322
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = RepositoryContext.class)
@ActiveProfiles("test")
@Slf4j
public class BigDataServiceImplTest {
@Autowired
@InjectMocks
// which has a field in BigDataServiceImpl
/*
@Autowired
private PosGatewayClient posGatewayClient;
*/
private BigDataService bigDataService;
private PosGatewayClient posGatewayClient;
@Before
public void init() {
posGatewayClient = Mockito.spy(new PosGatewayClient(Config.builder()
.endpoint("endpoint"))
.appId("appid"))
.appKey("appkey"))
.writeTimeoutMilliseconds(3000)
.build()));
MockitoAnnotations.initMocks(this);
}
@Test
public void redisAlreadyHasTheKey() throws IOException, NoSuchAlgorithmException {
boolean res = bigDataService.genernateBloomFilterAndUpload();
assertTrue(res);
}
}
I want to replace posGatewayClient
filed in BigDataServiceImpl
which implement the interface BigDataService
, but because MockitoTestExecutionListener
is ahead of SpringBootDependencyInjectionTestExecutionListener
, when initmocks
, the field bigDataService
is not autowire by spring, it's null, and the method checkNotInterface
in mockito class FieldInitializer
will fail.
Comment From: wilkinsona
Thanks. That wasn't quite what I was looking for, but I think I can see enough now to understand the problem.
Relying on Spring's dependency injection and then modifying its result with Mockito's injection was only ever working accidentally and isn't something that we would recommend. Instead, if you want a field in BigDataServiceImpl
to be initialised with a mock or spy, you should get Spring to inject it for you. Relying on Spring's dependency injection and then changing it will pollute the state of the application context. When the test framework caches the context and it's used in a subsequent test, that test will then encounter the spy unexpectedly.
You can either use @SpyBean
or you could use a nested @Configuration
class to define a PosGatewayClient
bean that is a spy.
If you have any further questions, please follow up on Stack Overflow or Gitter. As mentioned in the guidelines for contributing, we prefer to use GitHub issues only for bugs and enhancements.
Comment From: dmak
I have eventually come across the same issue, namely after this commit that reshuffles the listener order, injecting of spies into Spring-autowired beans is not possible out of the box.
Solutions: - Manually set the order of listeners in your test class:
@ExtendWith(SpringExtension.class)
@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, MockitoTestExecutionListener.class, ResetMocksTestExecutionListener.class})
public class MyTest {
}
- As suggested above by @wilkinsona, move your mock beans into the Spring context and let Spring wire them for you by using
@SpyBean
,@MockBean
or creating them manually (if some specific initialization is needed):
<bean id="restTemplate" class="org.mockito.Mockito" factory-method="mock" c:classToMock="org.springframework.web.client.RestTemplate" />
Relying on Spring's dependency injection and then changing it will pollute the state of the application context.
The process behind @SpyBean
actually does the same: at the end of the day it does not matter much who will inject spies into beans (Spring or Mockito). The more important is to control the lifecycle of spies, i.e. reset them after each test case and that is where Spring will be more helpful, however it can be done the same way explicitly in @AfterEach
method of the test.