How to test Spring Data repositories?
I want a repository (say, UserRepository
) created with the help of Spring Data. I am new to spring-data (but not to spring) and I use this tutorial. My choice of technologies for dealing with the database is JPA 2.1 and Hibernate. The problem is that I am clueless as to how to write unit tests for such a repository.
Let's take create()
method for instance. As I am working test-first, I am supposed to write a unit test for it - and that's where I bump into three problems:
-
First, how do I inject a mock of an
EntityManager
into the non-existing implementation of aUserRepository
interface? Spring Data would generate an implementation based on this interface:public interface UserRepository extends CrudRepository<User, Long> {}
However, I don't know how to force it to use an
EntityManager
mock and other mocks - if I had written the implementation myself, I would probably have a setter method forEntityManager
, allowing me to use my mock for the unit test. (As for actual database connectivity, I have aJpaConfiguration
class, annotated with@Configuration
and@EnableJpaRepositories
, which programmatically defines beans forDataSource
,EntityManagerFactory
,EntityManager
etc. - but repositories should be test-friendly and allow for overriding these things). Second, should I test for interactions? It is hard for me to figure out what methods of
EntityManager
andQuery
are supposed to be called (akin to thatverify(entityManager).createNamedQuery(anyString()).getResultList();
), since it isn't me who is writing the implementation.Third, am I supposed to unit-test the Spring-Data-generated methods in the first place? As I know, the third-party library code is not supposed to be unit-tested - only the code the developers write themselves is supposed to be unit-tested. But if that's true, it still brings the first question back to the scene: say, I have a couple of custom methods for my repository, for which I will be writing implementation, how do I inject my mocks of
EntityManager
andQuery
into the final, generated repository?
Note: I will be test-driving my repositories using both the integration and the unit tests. For my integration tests I am using an HSQL in-memory database, and I am obviously not using a database for unit tests.
And probably the fourth question, is it correct to test the correct object graph creation and object graph retrieval in the integration tests (say, I have a complex object graph defined with Hibernate)?
Update: today I've continued experimenting with mock injection - I created a static inner class to allow for mock injection.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@Transactional
@TransactionConfiguration(defaultRollback = true)
public class UserRepositoryTest {
@Configuration
@EnableJpaRepositories(basePackages = "com.anything.repository")
static class TestConfiguration {
@Bean
public EntityManagerFactory entityManagerFactory() {
return mock(EntityManagerFactory.class);
}
@Bean
public EntityManager entityManager() {
EntityManager entityManagerMock = mock(EntityManager.class);
//when(entityManagerMock.getMetamodel()).thenReturn(mock(Metamodel.class));
when(entityManagerMock.getMetamodel()).thenReturn(mock(MetamodelImpl.class));
return entityManagerMock;
}
@Bean
public PlatformTransactionManager transactionManager() {
return mock(JpaTransactionManager.class);
}
}
@Autowired
private UserRepository userRepository;
@Autowired
private EntityManager entityManager;
@Test
public void shouldSaveUser() {
User user = new UserBuilder().build();
userRepository.save(user);
verify(entityManager.createNamedQuery(anyString()).executeUpdate());
}
}
However, running this test gives me the following stacktrace:
java.lang.IllegalStateException: Failed to load ApplicationContext
at org.springframework.test.context.CacheAwareContextLoaderDelegate.loadContext(CacheAwareContextLoaderDelegate.java:99)
at org.springframework.test.context.DefaultTestContext.getApplicationContext(DefaultTestContext.java:101)
at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.injectDependencies(DependencyInjectionTestExecutionListener.java:109)
at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.prepareTestInstance(DependencyInjectionTestExecutionListener.java:75)
at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:319)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.createTest(SpringJUnit4ClassRunner.java:212)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner$1.runReflectiveCall(SpringJUnit4ClassRunner.java:289)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.methodBlock(SpringJUnit4ClassRunner.java:291)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:232)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:89)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)
at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:71)
at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:175)
at org.junit.runner.JUnitCore.run(JUnitCore.java:160)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:77)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:195)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:63)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userRepository': Error setting property values; nested exception is org.springframework.beans.PropertyBatchUpdateException; nested PropertyAccessExceptions (1) are:
PropertyAccessException 1: org.springframework.beans.MethodInvocationException: Property 'entityManager' threw exception; nested exception is java.lang.IllegalArgumentException: JPA Metamodel must not be null!
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1493)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1197)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:537)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:475)
at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:304)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:228)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:300)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:195)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:684)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:760)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:482)
at org.springframework.test.context.support.AbstractGenericContextLoader.loadContext(AbstractGenericContextLoader.java:121)
at org.springframework.test.context.support.AbstractGenericContextLoader.loadContext(AbstractGenericContextLoader.java:60)
at org.springframework.test.context.support.AbstractDelegatingSmartContextLoader.delegateLoading(AbstractDelegatingSmartContextLoader.java:100)
at org.springframework.test.context.support.AbstractDelegatingSmartContextLoader.loadContext(AbstractDelegatingSmartContextLoader.java:250)
at org.springframework.test.context.CacheAwareContextLoaderDelegate.loadContextInternal(CacheAwareContextLoaderDelegate.java:64)
at org.springframework.test.context.CacheAwareContextLoaderDelegate.loadContext(CacheAwareContextLoaderDelegate.java:91)
... 28 more
Caused by: org.springframework.beans.PropertyBatchUpdateException; nested PropertyAccessExceptions (1) are:
PropertyAccessException 1: org.springframework.beans.MethodInvocationException: Property 'entityManager' threw exception; nested exception is java.lang.IllegalArgumentException: JPA Metamodel must not be null!
at org.springframework.beans.AbstractPropertyAccessor.setPropertyValues(AbstractPropertyAccessor.java:108)
at org.springframework.beans.AbstractPropertyAccessor.setPropertyValues(AbstractPropertyAccessor.java:62)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1489)
... 44 more
tl;dr
To make it short - there's no way to unit test Spring Data JPA repositories reasonably for a simple reason: it's way to cumbersome to mock all the parts of the JPA API we invoke to bootstrap the repositories. Unit tests don't make too much sense here anyway, as you're usually not writing any implementation code yourself (see the below paragraph on custom implementations) so that integration testing is the most reasonable approach.
Details
We do quite a lot of upfront validation and setup to make sure you can only bootstrap an app that has no invalid derived queries etc.
- We create and cache
CriteriaQuery
instances for derived queries to make sure the query methods do not contain any typos. This requires working with the Criteria API as well as the meta.model. - We verify manually defined queries by asking the
EntityManager
to create aQuery
instance for those (which effectively triggers query syntax validation). - We inspect the
Metamodel
for meta-data about the domain types handled to prepare is-new checks etc.
All stuff that you'd probably defer in a hand-written repository which might cause the application to break at runtime (due to invalid queries etc.).
If you think about it, there's no code you write for your repositories, so there's no need to write any unittests. There's simply no need to as you can rely on our test base to catch basic bugs (if you still happen to run into one, feel free to raise a ticket). However, there's definitely need for integration tests to test two aspects of your persistence layer as they are the aspects that related to your domain:
- entity mappings
- query semantics (syntax is verified on each bootstrap attempt anyway).
Integration tests
This is usually done by using an in-memory database and test cases that bootstrap a Spring ApplicationContext
usually through the test context framework (as you already do), pre-populate the database (by inserting object instances through the EntityManager
or repo, or via a plain SQL file) and then execute the query methods to verify the outcome of them.
Testing custom implementations
Custom implementation parts of the repository are written in a way that they don't have to know about Spring Data JPA. They are plain Spring beans that get an EntityManager
injected. You might of course wanna try to mock the interactions with it but to be honest, unit-testing the JPA has not been a too pleasant experience for us as well as it works with quite a lot of indirections (EntityManager
-> CriteriaBuilder
, CriteriaQuery
etc.) so that you end up with mocks returning mocks and so on.
With Spring Boot + Spring Data it has become quite easy:
@RunWith(SpringRunner.class)
@DataJpaTest
public class MyRepositoryTest {
@Autowired
MyRepository subject;
@Test
public void myTest() throws Exception {
subject.save(new MyEntity());
}
}
The solution by @heez brings up the full context, this only bring up what is needed for JPA+Transaction to work. Note that the solution above will bring up a in memory test database given that one can be found on the classpath.