Lesson 7: Testing Spring Data Repositories
In this lesson, we're going to see how we can 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 what kind of test we can perform in order to check the correct behavior of the repositories' methods.
If we wanted to create Unit Tests, then we'd have to mock several JPA components that Spring loads. Naturally, that 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.
Therefore, 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 autoconfigurations that are used in our application. In this way, it closely replicates the real behavior of our application. Naturally, it allows us to customize these configurations if necessary as well.
Some of the features that @DataJpaTest provides to the tests are 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 see 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. Let's 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 to 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 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 when 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, and therefore, using some of its functions as helper methods would affect the scope of the test. For instance, we would want to test a query method but we would also be testing indirectly the persistence functionality. Therefore, an error in this test could be misleading.
2.4. Creating a Test Class
Let's now see the @DataJpaTest annotation and TestEntityManager class in action. We're going to test our ProjectRepository repository by creating a test class in the com.baeldung.lsd.persistence.repository package:
Note that we've annotated the class and injected the entity manager as well as the 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. Let's first see how to test the insertion:
As we can see, first 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 can see the following:
We can see the SQL statements executed during the test which undoubtfully can 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 will not be actually saved in our database and hence it won’t 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 of all, we'll persist a new Project object using the TestEntityManager bean as we did in the previous test. Then, we 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’ve 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 for examples of tests for our other repositories.