background

The team has recently been writing single tests with Mockito. Originally, they used @Mock and @InjectMocks to inject member properties. This method is written outside of SpringBoot Test and is ineffective in the case of single test cases where they want to call real Spring beans (such as database lookup). The Official SpringBoot documentation mentions that test supports Mockito, providing @MockBean and @SpyBean annotations that are used similarly to @Mock and @Spy, but can be used as part of the Spring Context. This allows you to Mock out some of the logic in a single test while still retaining spring’s functionality.

The problem

However, you may encounter circular dependencies when using @SpyBean. The code reproduced is as follows:

@SpringBootTest
public class MockitoTest {
    @SpyBean
    private DevComponent devComponent;
    @Autowired
    private TestComponent testComponent;
    @Test
    public void testMockito(a) {
        // ...}}@Component
public class DevComponent {
    @Autowired
    private TestComponent testComponent;
}
@Component
public class TestComponent {
    @Autowired
    private DevComponent devComponent;
}
Copy the code

The following error occurs:

Error creating bean with name ‘devComponent’: Bean with name ‘devComponent’ has been injected into other beans [testComponent] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching – consider using ‘getBeanNamesForType’ with the ‘allowEagerInit’ flag turned off, for example.

Analysis of the

In a previous article, How Spring getBean solves circular dependencies and Multiple Dynamic Proxies, we discussed the cyclic dependencies caused by @Transactional and @async. @ Transactional produced by AbstractAutoProxyCreator agent in getEarlyBeanReference () and postProcessAfterInitialization () will generate a proxy object, even if a Bean Both methods result in the same proxy object. @ Async produced by AbstractBeanFactoryAwareAdvisingPostProcessor agent, do not implement getEarlyBeanReference (), only postProcessAfterInitialization () If there is a circular dependencies, through getEarlyBeanReference () revealed early references (such as @ Transactional), and then postProcessAfterInitialization () and took a proxy Bean (such as @ Async), the two of them Proxy object, the loop dependency exception is reported. In the example above,MockitoPostProcessor and SpyPostProcessor handle the @MockBean and @SpyBean annotations. The key implementation of SpyPostProcessor is as follows:

@Override
public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
   // Return the new proxy object
   return this.mockitoPostProcessor.createSpyIfNecessary(bean, beanName);
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
   if (bean instanceof FactoryBean) {
      return bean;
   }
   // Return the new proxy object
   return this.mockitoPostProcessor.createSpyIfNecessary(bean, beanName);
}
Copy the code

Can see early exposure (getEarlyBeanReference ()) and normal agent (postProcessAfterInitialization ()) will produce different proxy objects, if the two beans rely on each other, will be submitted to the circular dependencies.

To solve?

Can, of course, set allowRawInjectionDespiteWrapping = true to solve, but don’t feel too elegant, easy to cause problems Would it be ok if SpyPostProcessor could use cache like AbstractAutoProxyCreator to proxy the same Bean only once? I put this question to submit to StackOverflow, a-minuses stackoverflow.com/questions/6…