Programming

How to test Spring Data repositories

27 September 2026 · 9 min read

How to test Spring Data repositories

Ensuring the reliability and correctness of your application’s data access layer is paramount for any robust software system. In the Spring ecosystem, Spring Data repositories abstract away much of the boilerplate code for database interactions, making development faster and cleaner. However, this abstraction doesn’t negate the need for thorough testing. Learning how to test Spring Data repositories effectively is a critical skill for any developer, guaranteeing that your application interacts with the database as expected, handles edge cases, and performs efficiently. This guide will walk you through various strategies, from focused unit tests to comprehensive integration tests, ensuring your data layer is solid and dependable.

Understanding Spring Data Repository Testing Approaches

When it comes to verifying the behavior of your Spring Data repositories, developers typically choose between two primary methodologies: unit testing and integration testing. Each approach serves a distinct purpose and offers different levels of confidence in your data access layer. Understanding when to apply each is crucial for an efficient and effective testing strategy.

Unit testing, in the context of repositories, primarily focuses on isolated components. While Spring Data repositories are interfaces with minimal custom logic, you might unit test custom repository implementations or complex query methods that have specific business logic. For standard CRUD operations, however, unit testing the repository interface itself often yields limited value, as the actual implementation is provided by Spring Data JPA at runtime. Here, tools like Mockito become invaluable for mocking the repository in service layer tests, ensuring that the service interacts correctly with its dependencies without needing a real database connection.

Conversely, integration testing is the gold standard for validating Spring Data repositories. It involves testing your repository alongside a real (or near-real) database environment to ensure that the queries are correctly formed, data is mapped accurately, and transactions behave as expected. Spring Boot provides excellent support for this with annotations like @DataJpaTest, which configures an in-memory embedded database (like H2) and auto-configures Spring Data JPA components. This approach gives high confidence that your repository methods will work correctly when deployed, making it an essential part of your Spring Boot testing suite.

Setting Up Your Repository Testing Environment

A well-configured testing environment is the bedrock of reliable repository tests. Spring Boot significantly simplifies this by providing the spring-boot-starter-test dependency, which bundles essential testing libraries like JUnit, AssertJ, Mockito, and Hamcrest. For data access layer testing, specifically, this starter also brings in Spring Test and other relevant components, allowing you to quickly set up transactional tests that interact with a database.

The core of an integration test for Spring Data repositories often involves the @DataJpaTest annotation. This annotation focuses on JPA components. It disables full auto-configuration and instead applies only configuration relevant to testing JPA applications. By default, it uses an in-memory embedded database, which is quick to start and tear down for each test, ensuring test isolation. This setup is ideal for verifying basic CRUD operations and simple queries, as it provides a lightweight and fast testing environment without the overhead of a full application context.

Embedded Databases vs. Testcontainers for Realistic Testing

While embedded databases like H2 or HSQLDB are excellent for fast and isolated repository tests, they sometimes fall short when your application relies on specific database features or dialects unique to your production database. This is where Testcontainers shine. Testcontainers provide lightweight, throwaway instances of databases, message brokers, web browsers, or anything else that can run in a Docker container. Using Testcontainers, you can run your repository tests against the exact same database engine (e.g., PostgreSQL, MySQL, Oracle) that you use in production, catching subtle compatibility issues that an embedded database might miss.

Integrating Testcontainers into your Spring Boot tests is straightforward. You typically add the Testcontainers dependency and configure your test to start a containerized database instance before tests run. This offers a higher fidelity testing environment, ensuring that your data access layer behaves identically across development, testing, and production. For complex applications with database-specific functions or queries, Testcontainers provide an invaluable layer of confidence, bridging the gap between local development and production deployment scenarios. According to a [tutorial](<https://spring.io/blog/2021/04/29/spring-boot-2-5-0-rc1-is Question & Answer :

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 <a href=>). 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 a UserRepository 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 for EntityManager, allowing me to use my mock for the unit test. (As for actual database connectivity, I have a JpaConfiguration class, annotated with @Configuration and @EnableJpaRepositories, which programmatically defines beans for DataSource, 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 and Query are supposed to be called (akin to that verify(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 and Query 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 a Query 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.