CrudRepository MockBean not injected into Component being tested
jtbeckha opened this issue · 18 comments
Testing this class
@Component
public class DemoComponent {
@Autowired
private DemoRepository demoRepository;
public DemoEntity getByAttribute(String attribute) {
return demoRepository.findFirstByAttribute(attribute);
}
}
where DemoRepository is a Spring Data JPA style repository
@Repository
public interface DemoRepository extends CrudRepository<DemoEntity, Long> {
DemoEntity findFirstByAttribute(String attribute);
}
This is the test
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoComponentTest {
@MockBean
private DemoRepository demoRepository;
@Autowired
private DemoComponent demoComponent;
@Test
public void testGetDemo_byAttribute() {
String attribute = "test";
when(demoRepository.findFirstByAttribute(attribute)).thenReturn(new DemoEntity(attribute));
demoComponent.getByAttribute(attribute);
assertNotNull(demoComponent.getByAttribute(attribute));
}
}
The test fails, because the demoRepository mock isn't being injected into demoComponent during the test.
FWIW I figured out that if I remove the CrudRepository superclass from DemoRepository the test passes so it could be related to that
I believe the problem is that we end up with two DemoRepository
beans, one named demoRepository
(the actual Data JPA repository) and one named your.package.DemoRepository#0
(the mock). Normally this would result in a NoUniqueBeanDefinitionException
, however the injection point for DemoRepository
in DemoComponent
is named demoRepository
and this matches the name of the actual Data JPA repository bean so it is this bean that is injected. @MockBean
should result in the actual Data JPA repository being replaced with the mock. This replacement isn't happening and appears to be the root of the problem.
@jtbeckha There's a small amount of guess work in my analysis above. A complete example that I can run would remove that guess work and ensure that I'm actually fixing the problem that you are seeing.
MockitoPostProcessor
calls ListableBeanFactory.getBeanNamesForType(Class<?>)
. This returns an empty array despite there being a JpaRepositoryFactoryBean
registered with the bean factory that produces a bean of the required type. This factory bean doesn't match as its repository interface hasn't been set at this stage so getObjectType()
returns Repository.class
rather than DemoRepository.class
.
This factory bean doesn't match as its repository interface hasn't been set at this stage so getObjectType() returns Repository.class rather than DemoRepository.class.
This isn't quite right. It works later on as FactoryBeanTypePredictingBeanPostProcessor
has been registered and correctly predicts the type of the JpaRepositoryFactoryBean
allowing the bean that it produces (a DemoRepository
) to be injected.
In short, the problem is that the type of a JpaRepositoryFactoryBean
cannot be predicted from within MockitoPostProcessor
(a BeanFactoryPostProcessor
) as the prediction relies upon FactoryBeanTypePredictingBeanPostProcessor
(a SmartInstantiationAwareBeanPostProcessor
) which has not yet been registered.
@jtbeckha Assuming that my analysis applies to your problem, you can work around it by explicitly setting the name of your mocked bean to match the name of the bean created by Spring Data JPA:
@MockBean(name="demoRepository")
private DemoRepository demoRepository;
This short-circuits the logic that currently fails to find the existing bean and ensures that it's overridden.
This is a very similar problem to #2811. We could make use of the same solution here if Spring Data set the factoryBeanObjectType
attribute on the JpaRepositoryFactoryBean
bean definitions.
@wilkinsona Thanks a lot for looking into this, your workaround of setting the name works. I have uploaded a complete example here https://github.com/jtbeckha/spring-boot-6541
@jtbeckha Thanks for the example. It's confirmed that I was looking at the same problem. It should be fixed in now. A 1.4.1 snapshot will be available just a soon as Bamboo manages to get a reliable network connection to Maven Central.
@wilkinsona , the similar story for the @MockBean
nested dependencies:
@RunWith(SpringRunner.class)
@WebMvcTest(TestController.class)
public class TestControllerTest {
@MockBean
private TestRequestValidator testRequestValidator;
@MockBean
private TestService testService;
@Autowired
private TestRestTemplate restTemplate;
@Test
public void test() throws Exception {
log.info("Test starting...");
});
}
... where's the mocked bean is:
@Component
public class TestRequestValidator implements Validator {
@Autowired
private CategoryRepository categoryRepository;
...
}
during test startup it fails:
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'testController': Unsatisfied dependency expressed through field 'testRequestValidator': Error creating bean with name 'com.testproject.validators.TestRequestValidator#0': Unsatisfied dependency expressed through field 'categoryRepository': No qualifying bean of type [com.testproject.repositories.CategoryRepository] found for dependency [com.testproject.repositories.CategoryRepository]: expected at least 1 bean which qualifies as autowire candidate for this dependency.
Seems to be the mocked beans are not being mocked&injected to the component.
@WildDeveloper I can't tell if that's the same problem or a different one. Can you please try 1.4.1.BUILD-SNAPSHOT that's available from https://repo.spring.io/libs-snapshot? If the failure still occurs then it's a different problem and should be tackled in a separate issue, ideally with a sample project that reproduces the problem.
@wilkinsona , 1.4.1.BUILD-SNAPSHOT didn't help. The issue is reported. #6663
We have a quite similar problem with Spring-Boot 1.5.7, a regression maybe? See https://stackoverflow.com/questions/49303080/spring-boot-application-context-with-arangodb-repository-cannot-be-created-if-us for details
@mark-- That looks like a different problem to me.
The ArangoDB Spring Data integration is rather atypical. ArangoRepositoryFactoryBean
is using constructor injection for both the repository interface and for ArangoOperations
. By contrast, all of Spring Data's own repository factory beans inject the repository interface into the constructor but then use setter injection for everything else. This aligns them with the expectations of RepositoryBeanDefinitionBuilder
which only specifies a single constructor argument when defining the bean.
@wilkinsona OK, but the workaround of giving the MockBean some name also fixes the error. Anyway,
do you think I should report this as bug to the ArangoDB project?
@mark-- Providing the name stops a certain call to the bean factory from being made. If anything else happened to make a similar call the problem would still occur. I think a bug report against the ArangoDB project is the right way to proceed.
@wilkinsona The Arango team fixed this problem, see arangodb/spring-data#14 (comment)
Thank you for your support!
Excellent. Thanks for letting us know.
@wilkinsona same problem happened with me, but my inner beans are related to soap services (WebServiceGatewaySupport), also I have tried all the above-mentioned methods and nothing solved the issue.
@amcghie If you believe you have found a bug in Spring Boot, please open a new issue with a minimal sample project that reproduces the problem. If you're looking for some help or advice, please come and chat on Gitter or ask a question on Stack Overflow.