Hello! Welcome back to our series on backend development.
In the last lesson, we focused on "slice testing" the web layer with @WebMvcTest. You learned how to simulate HTTP requests and assert responses while mocking the service layer underneath. This allowed us to test our controllers in isolation.
Today, we're moving down the stack to the persistence layer. Our goal is to write integration tests for the repository layer using @DataJpaTest. This is the final piece of our "slice testing" puzzle. We'll learn how to verify that our Spring Data JPA repositories correctly interact with a database, ensuring our queries and CRUD operations work as intended.
1. The Data JPA Test Slice
Just as @WebMvcTest creates a slice of the application context for the web layer, @DataJpaTest does the same for the persistence layer. It sets up a minimal Spring environment specifically for testing our JPA components.
Let's dive into what @DataJpaTest does for us.
Testing JPA Queries with Spring Boot and @DataJpaTest
The article 'Testing JPA Queries with Spring Boot and @DataJpaTest' from reflectoring.io offers a superb conceptual overview. We'll read the section that explains precisely what @DataJpaTest does under the hood.
Please read the section titled '@DataJpaTest in a Nutshell'. Focus on these key points: It creates a 'slice' of your application context, including only JPA components. It auto-configures an in-memory database (like H2) by default, keeping tests fast and independent of your real database. It makes each test method run in its own transaction, which is rolled back at the end. This is a critical feature that ensures tests are isolated from one another.
This automatic rollback is a fundamental concept. It means any data you save or modify in one test will be erased before the next test begins. This prevents tests from interfering with each other and ensures they are reliable and repeatable.
2. Writing Your First Repository Test
Now, let's get practical. We'll see how to set up a test class and write tests for basic CRUD (Create, Read, Update, Delete) operations. The general structure of a test class looks like this:
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
@DataJpaTest // 1. The key annotation for this test slice
class EmployeeRepositoryTest {
@Autowired // 2. Spring injects the repository we want to test
private EmployeeRepository employeeRepository;
// 3. Test methods will go here
@Test
void myFirstTest() {
// ...
}
}
The video below by Java Guides provides a complete, step-by-step walkthrough of writing tests for a JpaRepository.
CRUD JUnit Tests for Spring Data JPA | Testing Repository Layer | Spring Boot | @DataJpaTest
This video, 'CRUD JUnit Tests for Spring Data JPA', is an excellent hands-on guide. We will watch it in segments to build our understanding of testing each type of repository operation.
First, watch from 00:12:20 to 00:22:27. This part covers: Setting up the test class: Annotating with @DataJpaTest and autowiring the EmployeeRepository. Testing the save() method: Creating an entity, saving it, and asserting that it was persisted correctly (e.g., by checking if an ID was generated). Testing findById() and findAll(): Retrieving one or more entities and asserting their contents.
Deconstructing the Read/Write Tests
Let's analyze the patterns from the video.
Testing a save() operation:
The goal is to ensure that when we call save(), the entity is correctly persisted in the database. A key indicator is that the database generates a primary key for the new entity.
@Test
void givenEmployeeObject_whenSave_thenReturnSavedEmployee() {
// Arrange: Create an Employee object
Employee employee = new Employee("Ramesh", "Fadatare", "ramesh@gmail.com");
// Act: Save the employee to the database
Employee savedEmployee = employeeRepository.save(employee);
// Assert: Verify the returned object is not null and has an ID
assertThat(savedEmployee).isNotNull();
assertThat(savedEmployee.getId()).isGreaterThan(0);
}
Testing a findById() operation:
Here, we first save an entity so there's data to retrieve. Then we use its ID to find it and verify that the correct data is returned.
@Test
void givenEmployeeId_whenFindById_thenReturnEmployeeObject() {
// Arrange: Save an employee first
Employee employee = new Employee("Ramesh", "Fadatare", "ramesh@gmail.com");
employeeRepository.save(employee);
// Act: Find the employee by its ID
Employee foundEmployee = employeeRepository.findById(employee.getId()).get();
// Assert: Verify the found employee is not null
assertThat(foundEmployee).isNotNull();
assertThat(foundEmployee.getFirstName()).isEqualTo("Ramesh");
}
3. Testing Update and Delete Operations
Testing updates and deletions follows a similar pattern: set up the state, perform the action, and assert the outcome.
CRUD JUnit Tests for Spring Data JPA | Testing Repository Layer | Spring Boot | @DataJpaTest
Let's continue with the same Java Guides video to see how update and delete operations are tested.
Now, please watch from 00:22:27 to 00:31:06. This segment demonstrates: Testing an update: Retrieving an entity, changing a property, saving it again, and asserting that the change was persisted. Testing a delete: Retrieving an entity, deleting it, and then attempting to find it again to assert that it no longer exists.
Testing an update() operation:
Spring Data JPA's save() method handles both creating and updating. If you call save() on an entity that already has a primary key and exists in the database, Hibernate will perform an update.
@Test
void givenEmployeeObject_whenUpdateEmployee_thenReturnUpdatedEmployee() {
// Arrange: Save an employee
Employee employee = new Employee("Ramesh", "Fadatare", "ramesh@gmail.com");
employeeRepository.save(employee);
// Act: Retrieve it, change a value, and save again
Employee savedEmployee = employeeRepository.findById(employee.getId()).get();
savedEmployee.setEmail("ram@gmail.com");
Employee updatedEmployee = employeeRepository.save(savedEmployee);
// Assert: Verify the email has been updated
assertThat(updatedEmployee.getEmail()).isEqualTo("ram@gmail.com");
}
Test your understanding!
Imagine your EmployeeRepository has a custom query method: Optional<Employee> findByEmail(String email);. How would you write a test to verify this method works correctly? Outline the steps.
Show answer
-
Arrange:
- Create a new
Employeeobject with a unique email, for example,test@example.com. - Save this employee using
employeeRepository.save().
- Create a new
-
Act:
- Call the custom method:
Optional<Employee> foundEmployeeOpt = employeeRepository.findByEmail("test@example.com");
- Call the custom method:
-
Assert:
- Assert that the returned
Optionalis not empty:assertThat(foundEmployeeOpt).isPresent(); - Get the employee from the optional and assert that its properties match the one you saved:
assertThat(foundEmployeeOpt.get().getFirstName()).isEqualTo(...).
- Assert that the returned
4. Advanced Concepts: Test Dependencies and TestEntityManager
Test Isolation vs. Test Order
In the video (00:31:06 onwards), the presenter runs all tests together and finds they fail. This is because by default, JUnit does not guarantee the execution order of tests. His tests were written to depend on each other (e.g., the "get" test assumes the "save" test has already run).
He solves this with @TestMethodOrder and @Rollback(false). However, this is not standard practice. The default behavior of @DataJpaTest (isolated, transactional tests) is what you should almost always use. Writing independent tests makes your test suite more robust and easier to maintain. You should set up the required data within each test itself, as shown in our code examples above.
Using TestEntityManager
Sometimes, you need more fine-grained control over the persistence context, especially when testing complex entity relationships. For this, Spring provides TestEntityManager. It's an alternative to using the repository for setting up data and is specifically designed for tests.
How to Test Repositories in Spring Boot
The 'Code Java' channel has a great video that explains the role of TestEntityManager. We'll watch a segment where it's used to set up a dependent entity.
Please watch from 00:25:20 to 00:29:15. The presenter is testing a StateRepository. Since a State must belong to a Country, he uses TestEntityManager to persist a Country object first, without involving the CountryRepository. This is a great example of its utility.
You can autowire it just like your repository:@Autowired private TestEntityManager entityManager;
And then use it to persist and flush data:Country savedCountry = entityManager.persistAndFlush(new Country("Vietnam"));
The persistAndFlush() method saves the entity and immediately synchronizes the changes with the in-memory database, which can be useful in certain scenarios. For most standard repository tests, however, using the repository itself is perfectly sufficient.
Conclusion
Congratulations! You've now learned how to test the third and final layer of our application in a focused "slice". By mastering @DataJpaTest, you can write fast, reliable tests that give you confidence in your data access logic.
Key Takeaways:
@DataJpaTestis the annotation for creating integration tests that focus solely on the persistence layer.- It automatically configures an in-memory database and only loads JPA-related beans (
@Entity,JpaRepository). - Tests are transactional and roll back by default, which is crucial for maintaining test isolation.
- You can test repository methods by setting up data, executing the method, and using a library like AssertJ to verify the outcome.
What's Next?
We've tested the web, service, and data layers in isolation. But how do we ensure they all work together correctly? In the next lesson, we'll learn how to write end-to-end tests for the application using @SpringBootTest and TestRestTemplate. This will involve starting the entire Spring application and testing it as a whole, from the HTTP request all the way down to the database and back.