Lesson 7: Testing Spring Data Repositories
In this lesson, we're going to learn how to test Spring Data repositories’ methods using a convenience annotation provided by Spring Boot.
2. Lesson Notes
The relevant module you need to import when you're starting with this lesson is: testing-spring-data-repositories-start
If you want to have a look at the fully implemented lesson, as a reference, feel free to import: testing-spring-data-repositories-end
2.1. Unit Test or Integration Test
Let’s quickly analyze the kinds of tests we can perform in order to check the correct behavior of the repositories’ methods.
If we want to create Unit Tests, then we have to mock several JPA components that Spring loads. Naturally, this would affect the functionality of the repository. Therefore, Unit Tests don't make much sense in this case.
On the other hand, when we test Spring Data repositories, we usually want to verify that our queries retrieve the expected data from a database when running a SELECT operation. When executing UPDATE, DELETE, and INSERT operations, we want to verify that our queries get executed successfully, maintaining the consistent state of our database.
Consequently, we need integration tests, as the validation exceeds the bare Java code we actually develop.
2.2. Auto-configured Data JPA Tests
Spring Boot provides the @DataJpaTest annotation to test our repositories. By default, this annotation invokes several Spring Boot auto-configurations that are used in our application. In this way, it closely replicates the real behavior of our application. Of course, it also allows us to customize these configurations if necessary.
Some of the features that @DataJpaTest provides for the tests include enabling the scanning of our @Entity classes, and configuring the Spring Data JPA repositories. Additionally, it enables the logging of the database queries that are executed, so that we can clearly see what's going on in the tests.
Naturally, this annotation doesn't configure all the layers when setting up the application context. Instead, it configures only those that are suitable for data access tests.
The data.sql initialization script is also executed for our tests, but we won't use these records in our test examples. Instead, we'll opt for a different approach, using one more feature provided by the annotation we're analyzing here. We'll explore this in detail in the next section.
With this annotation, we don’t need to declare transaction scopes in a test. By default, all tests decorated with the @DataJpaTest annotation become transactional. Moreover, the transaction rolls back at the end of each test, so as not to affect the initial database state of other tests.
There are use cases, however, in which we might not want to follow this approach. We’ll leave aside the analysis of the possible pitfalls of this approach, but it’s worth knowing that we can disable the default transaction management by adding an extra annotation:
2.3. Injecting a TestEntityManager
When working with Spring Data JPA, we don’t usually talk to the EntityManager directly. Our repositories will use a configured EntityManager to interact with the database.
However, in order to populate the database with the test data or verify the test results, we can also inject a TestEntityManager bean in our tests, which as you can guess, is configured when we add the @DataJpaTest annotation.
Alternatively, we can use the repositories' methods for this purpose, but we recommend using a TestEntityManager, as sometimes the repositories offer a limited set of operations. One of the possible scenarios might be a situation where we need to test a read-only repository, but we need to add records in the database in order to execute the tests successfully.
Moreover, we usually want to test each repository feature separately; therefore, using some of its functions as helper methods would affect the scope of the test. For instance, we may want to test a query method, but we would also be indirectly testing the persistence functionality. As a result, an error in this test could be misleading.
2.4. Creating a Test Class
Now let's see the @DataJpaTest annotation and TestEntityManager class in action. We'll test our ProjectRepository repository by creating a test class in the com.baeldung.lsd.persistence.repository package:
Note that we annotated the class, and injected the entity manager and repository we'll be testing.
2.5. Testing the Repository Methods
A repository can offer various methods that allow us to perform different queries against our database. Let's demonstrate how we can test them.
Testing the Insertion Operation
As we know, the repositories’ save() method can handle two operations: database insert and update. First, let's see how to test the insertion:
As we can see, we create a new transient Project object, and then we call the repository's save() method. After that, we can find the newly inserted object using the injected TestEntityManager, and we expect it to be equal to the newProject instance.
Let's run the test, and make sure that it passes. If we check the console log, among other messages we’ll see the following:
We can see the SQL statements executed during the test, which can undoubtedly be helpful when we’re hunting query problems. In addition, we're informed that the test transaction will be rolled back, so that the inserted data won’t actually be saved in our database and affect other tests.
Testing the Update Operation
Similarly, we can test the update functionality:
First, we prepare the newProject instance and persist it, this time using the entityManager bean. Then we change its name, and call the repository's save() method. Finally, we verify that the name has been updated in the database.
Testing the findById Method
Now let’s test the findById() method. First, we'll persist a new Project object using the TestEntityManager bean, like we did in the previous test. Then we’ll call the repository's findById() method, and verify the returned value:
Testing Other findBy... Queries
We can easily test any other read methods defined in our repository in the way described in the previous section. For example, in our repository, we defined the findByNameContaining() method, which retrieves an Iterable collection of entities, instead of an Optional. We can test it as follows:
Testing the Delete Operation
Finally, let's test the delete() method. Once again, we create a Project entry in the database, and then we call our repository's delete() method. After that, we verify if the data entry has been removed:
You can check the codebase to see examples of tests for our other repositories.