How to test the Data Layer of your Spring Boot Application with @DataJpaTest


In this Blogpost, I will show you how to test the JPA-based data layer of your Spring Boot Application with @DataJpaTest. You will learn what happens when you use this annotation on a test class and a few ways to customize the default behavior. We will also have a look at an example where we will bootstrap some test data in different ways and write an actual JPA test.

What is @DataJpaTest?

@DataJpaTest is an annotation that is used to test the JPA components of your application. Applied to a test class it will disable the full-auto-configuration and instead configure only those components, that are relevant for JPA tests. For example @JpaComponent, @Repository, and @Entity. It is also a meta-annotation of @Transactional which makes the tests transactional by default. The transactions will roll back at the end of each test, By default. If there is an embedded in-memory database (e.g. H2) on the classpath, it will be used for the test. If you rather want a connection to your real infrastructure, you can override this behavior by applying @AutoConfigureTestDatabase to the test. Although @DataJpaTest is all about testing your JPA components there will also be a configured JdbcTemplate. This can be useful if you want to work directly with your database to set things up or send queries.

To use @DataJpaTest, we need to include the dependency org.springframework.boot:spring-boot-starter-test in our project.

pom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

We will also add com.h2database:h2 as a dependency to use H2 as an in-memory database for our tests.

pom.xml
<dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
</dependency>

The Example

In our example, we have Restaurants that can be rated. The rating is a simple number between 0 and 10. The higher the number, the better the rating. Let’s assume we have a use case that needs only the top-rated restaurants in our database – maybe those should be displayed on the start page of the application. A top-rated restaurant is one with a rating of at least eight.

Here are the entity classes:

Restaurant.java
@Entity
@Table(name = "restaurant")
public class Restaurant {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(cascade = CascadeType.ALL, mappedBy = "restaurant")
    private Set<Rating> ratings;

    // constuctor, getter and setter omitted
}

Rating.java
@Entity
@Table(name= "rating")
public class Rating {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(optional = false)
    private Restaurant restaurant;

    private Integer score;

    // constuctor, getter and setter omitted
}

This repository contains the logic to retrieve the top-rated restaurants from the database which we want to test:

RestaurantRepository.java
public interface RestaurantRepository extends JpaRepository<Restaurant, Long> {

    @Query(value = "SELECT r FROM Restaurant r LEFT JOIN r.ratings ra GROUP BY r having avg(ra.score) >= 8")
    List<Restaurant> findTopRatedRestaurants();

}

Bootstrapping Test Data

Regardless of what kind of automated test we write, there is always one task we have to do before executing the thing we want to test – put the system into a known state. Often this phase in the lifecycle of a test is called Setup- or Bootstrapping phase. In a unit test, this is done by instantiating some objects. In the kind of integration test, we are going to write, it also means putting some test data into our database. Luckily for us, Spring Boot supports some different ways to do this.

Using the data.sql File

Although this method is not exclusively for integration tests, you can bootstrap test data for your database by providing a file called data.sql on the classpath. As the extension of the file implies, you can write plain SQL.

src/test/resources/data.sql
INSERT INTO restaurant (name) VALUES ('Café Java');
INSERT INTO rating (restaurant_id, score) VALUES (1, 10);

I suggest using this method only for data that is required for all tests.

Using the @Sql Annotation

Spring’s @Sql annotation allows us to load an SQL file for specific tests.

RestaurantTest.java
@DataJpaTest
public class RestaurantTest {

    @Test
    @Sql("/fixture/test.sql")
    void test() {

    }
}

In this example, the SQL statements in /src/test/resources/fixture/test.sql will be executed before the test method. We can also apply @Sql to the test class in which case the statements would be executed before each test in the class. By default, a declaration on the test method will override the one on the test class. You can change this behavior by applying the @SqlMergeMode annotation.

RestaurantTest.java
@DataJpaTest
@Sql("/fixture/foo.sql")
public class RestaurantTest {

    @Test
    @Sql("/fixture/test.sql")
    @SqlMergeMode(SqlMergeMode.MergeMode.MERGE) // will result in the execution of foo.sql and test.sql
    void test() {

    }
}

Using the JdbcTemplate

As mentioned above Spring Boot will configure a JdbcTemplate for tests with the @DataJpaTest annotation. This means we can use it to bootstrap the data required for our test.

RestaurantTest.java
@DataJpaTest
public class RestaurantTest {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Test
    void test() {
        jdbcTemplate.update("INSERT INTO restaurant (id, name) VALUES (1, 'Café Java');");
        // ...
    }
}

Using SpringData Repositories

Another way to bootstrap our test data is to use the repositories of our application.

RestaurantTest.java
@DataJpaTest
public class RestaurantTest {

    @Autowired
    private RestaurantRepository restaurantRepository;

    @Test
    void test() {
        var restaurant = new Restaurant("Café Java");
        restaurantRepository.save(restaurant);
        // ...
    }
}

This approach has the disadvantage that you rely on the functionality of the component you want to test. I suggest using this method only if you already know that your save method works as expected.

Writing the Test

The test case we are going to write should prove that RestaurantRepository::findTopRatedRestaurants() retrieves only those restaurants, with an average rating of at least 8. To put our system in a known state we will provide the following SQL file:

fixture/restaurantTest_topRatedRestaurant.sql
INSERT INTO restaurant (name) VALUES ('Café Java');
INSERT INTO rating (restaurant_id, score) SELECT id, 10 FROM restaurant WHERE name = 'Café Java';

INSERT INTO restaurant (name) VALUES ('Spring Restaurant');
INSERT INTO rating (restaurant_id, score) SELECT id, 10 FROM restaurant WHERE name = 'Spring Restaurant';
INSERT INTO rating (restaurant_id, score) SELECT id, 6 FROM restaurant WHERE name = 'Spring Restaurant';

INSERT INTO restaurant (name) VALUES ('Jakarta Restaurant');
INSERT INTO rating (restaurant_id, score) SELECT id, 8 FROM restaurant WHERE name = 'Jakarta Restaurant';
INSERT INTO rating (restaurant_id, score) SELECT id, 7 FROM restaurant WHERE name = 'Jakarta Restaurant';

This will create three restaurants:

  • “Café Java” with an average rating of 10
  • “Spring Restaurant” with an average rating of 8
  • “Jakarta Restaurant” with an average rating of 7.5

With this data, we expect to retrieve 2 restaurants from our method – “Café Java” and “Spring Restaurant”. The “Jakarta Restaurant” should not be included because its average rating of 7.5 is under the threshold of 8.

Let’s start writing the actual test code:

RestaurantTest.java
@DataJpaTest
public class RestaurantTest {

    @Autowired
    private RestaurantRepository restaurantRepository;

    @Test
    @Sql("/fixture/restaurantTest_topRatedRestaurant.sql")
    void restaurantsWithAnAverageRatingOfEight_AreTopRatedRestaurants() {
        List<Restaurant> topRestaurants = restaurantRepository.findTopRatedRestaurants();
        assertEquals(2, topRestaurants.size());
    }
}

We are using Spring’s @Autowired to inject the RestaurantRepository – the component we are going to tests. This works because our test class is annotated with @DataJpaTest which leads to the configuration of all JPA components. We are also using the @Sql annotation to load the test data. The remaining part of the test is fairly simple. We execute the method we want to test and make an assertion about the result.

Teardown after the Test

When writing any kind of automated test, we need to make sure that all tests are independent of one another. If one test writes data into the database, it could potentially break the next test which does not expect any existing data. Therefore, we need to delete the data that is created by our tests. In the case of these JPA tests, this is not completely true because we do not need to do anything – Spring will do it for us. As mentioned earlier @DataJpaTest is a meta-annotation of @Transactional. This means all data written into the database will be automatically removed by a rollback at the end of each test.

Resources