Hello! Welcome to the next lesson in our journey through Spring testing.
In the previous lesson, we focused on unit testing the service layer. We learned how to use Mockito to create mock repositories, allowing us to test our service's business logic in complete isolation. We operated under the assumption that the repository itself worked as intended.
Today, we're going to test that assumption. This lesson addresses the learning outcome: Write an integration test for the data layer using @DataJpaTest and an in-memory database. We'll shift from unit testing with mocks to writing focused integration tests for our Spring Data JPA repositories. This type of test verifies the integration between your repository code, the JPA provider (Hibernate), and a database, ensuring your queries behave exactly as you expect.
1. From Unit Tests to Integration "Slice" Tests
When we unit-tested the ProductService, we deliberately ignored the database. Now, we want to do the opposite: test the interaction with the database but ignore the service and controller layers.
Spring Boot makes this easy with "slice tests". Instead of loading the entire application, we load only the specific "slice" we need. For the persistence layer, the magic annotation is @DataJpaTest.
2. Understanding @DataJpaTest
The @DataJpaTest annotation is a specialized test configuration that does several things for us automatically:
- Scans for JPA Components: It finds your
@Entityclasses and Spring Data@Repositoryinterfaces. - Configures an In-Memory Database: It looks for an in-memory database on the classpath (like H2, HSQLDB, or Derby) and automatically configures a
DataSourceto connect to it. This provides a fast, temporary database for each test run. - Configures JPA and Hibernate: It sets up all the necessary beans for JPA, such as the
EntityManagerandTransactionManager. By default, it configures Hibernate to automatically create the database schema from your entities (ddl-auto: create-drop). - Provides Transactional Tests: Each test method is wrapped in a transaction that is rolled back by default at the end of the method's execution. This is a crucial feature, as it ensures that each test runs against a clean, predictable database state and that tests do not interfere with one another.
- Restricts the Application Context: It does not load other beans like
@Service,@Controller, or other@Components. This keeps the test lightweight and focused purely on the data access layer.
To learn more about what @DataJpaTest does behind the scenes, the following article provides a concise and clear explanation.
Testing JPA Queries with Spring Boot and @DataJpaTest
The article 'Testing JPA Queries with Spring Boot and @DataJpaTest' gives an excellent summary of how this slice test works.
Read the section titled '@DataJpaTest in a Nutshell'. It provides a clear, high-level overview of the components it configures and its default transactional behavior.
3. Setting Up the Test Environment
To write a data layer integration test, you need two things in your project:
- The
spring-boot-starter-testdependency (which should already be in yourpom.xml). - A dependency for an in-memory database. We'll use H2.
Add the following to your pom.xml within the <dependencies> block:
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
The <scope>test</scope> tag is important; it ensures that the H2 database library is only available during the test phase and is not included in your final application package.
By default, @DataJpaTest will now auto-configure this H2 database. You don't need any extra configuration files for a basic setup.

4. Writing a Repository Integration Test
Let's write a test for a ProductRepository. Assume we have this repository, which extends JpaRepository and has one custom query method.
The Repository Interface:
public interface ProductRepository extends JpaRepository<Product, Long> {
// Custom query method we want to test
List<Product> findByCategory(String category);
}
The built-in methods like save(), findById(), and findAll() are provided and tested by the Spring Data team. Our primary goal is to test the custom queries we write ourselves.
The following video provides a fantastic, practical walkthrough of this entire process.
Software Testing Tutorial - Learn Unit Testing and Integration Testing
The video 'Software Testing Tutorial' by Amigoscode demonstrates exactly how to set up and write a @DataJpaTest from scratch.
Watch from 20:58 to 42:08. This segment covers: Setting up the test class: How to annotate the class with @DataJpaTest and autowire the repository under test. Configuring H2: How to add the dependency and, if needed, create a test-specific application.properties file (though often not required for basic H2 use). Writing tests: Implementing the Given-When-Then pattern to test a custom repository method for both a success and a failure case. Cleanup: Using an @AfterEach method to ensure a clean state, which complements the default rollback behavior.
Let's solidify this with our own example. Here is the test class setup:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
// 1. Use @DataJpaTest to set up the JPA test slice
@DataJpaTest
class ProductRepositoryTest {
// 2. Autowire the repository we want to test. Spring will provide a fully-configured instance
// connected to the in-memory H2 database.
@Autowired
private ProductRepository productRepository;
// ... Test methods will go here
}
Test 1: Testing a Custom Query
Now, let's test our custom findByCategory method. We'll use the Arrange-Act-Assert pattern.
import static org.assertj.core.api.Assertions.assertThat;
// ... inside ProductRepositoryTest
@Test
void findByCategory_shouldReturnProductsInCategory() {
// Arrange: Save some test data.
// Because tests are transactional and rolled back, this data exists only for this method.
productRepository.save(new Product("Laptop", "Electronics", 1200.00));
productRepository.save(new Product("Keyboard", "Electronics", 75.00));
productRepository.save(new Product("Coffee Mug", "Kitchen", 15.00));
// Act: Call the custom method we are testing.
List<Product> electronics = productRepository.findByCategory("Electronics");
// Assert: Verify the results.
assertThat(electronics).isNotNull();
assertThat(electronics).hasSize(2);
// You can perform more detailed assertions on the contents of the list
assertThat(electronics).extracting(Product::getName).containsExactlyInAnyOrder("Laptop", "Keyboard");
}
Test 2: Testing a "Not Found" Scenario
It's equally important to test that our query correctly returns nothing when no matching data exists.
@Test
void findByCategory_shouldReturnEmptyList_whenNoProductsInCategory() {
// Arrange: Save some data that does NOT match the query criteria.
productRepository.save(new Product("Coffee Mug", "Kitchen", 15.00));
// Act: Call the method with a category that has no products.
List<Product> electronics = productRepository.findByCategory("Electronics");
// Assert: Verify the list is empty.
assertThat(electronics).isNotNull();
assertThat(electronics).isEmpty();
}
After running these tests in your IDE, you should see them all pass.
5. Advanced Topic: In-Memory vs. Real Databases
Using an in-memory database like H2 is fast and convenient. However, it's not a perfect replica of a production database like PostgreSQL or MySQL. If you write a native SQL query (@Query(nativeQuery = true, ...)), it might use a function specific to PostgreSQL that doesn't exist in H2. Your test would pass, but your application would fail in production.
For an experienced developer interested in robust system design, this is a critical point. The best practice for high-fidelity testing is to run your tests against the same type of database you use in production. The Testcontainers library is the modern solution to this problem. It programmatically starts a real database (e.g., PostgreSQL) in a Docker container just for your test run.
We won't implement Testcontainers in this course, but it's the logical next step for production-grade persistence testing.
Testing the Persistence Layer With Spring Boot ...
For a deeper dive into this topic, the article 'Testing the Persistence Layer With Spring Boot' discusses the drawbacks of in-memory databases and introduces Testcontainers as the solution.
Read the section titled 'Test Against a Real Database'. It concisely explains the problem and how Testcontainers solves it.
Conclusion
Today we took a major step from isolated unit tests to focused integration tests. You learned how to test the critical persistence layer of your application, ensuring your custom queries and repository logic are correct.
Key Takeaways:
@DataJpaTestcreates a specialized "slice" of your application context for testing the persistence layer.- It automatically configures an in-memory database (like H2) and manages transactions.
- By default, tests are transactional and rolled back after execution, guaranteeing test isolation.
- You should focus your tests on your own custom queries, as the methods provided by
JpaRepositoryare already tested by Spring. - While in-memory databases are fast, for the highest fidelity, testing against a real database using a library like Testcontainers is the recommended practice.
In our next lesson, we will move one layer up the stack to learn how to write an integration test for a REST controller using @SpringBootTest and MockMvc. This will allow us to test our API endpoints by simulating HTTP requests and verifying the responses.