24

Test Your Spring Boot JPA Persistence Layer With @DataJpaTest

 3 years ago
source link: https://rieckpil.de/test-your-spring-boot-jpa-persistence-layer-with-datajpatest/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Test Your Spring Boot JPA Persistence Layer With @DataJpaTest

Published: November 24, 2020Last Updated on December 9, 2020

First time here? Get an overview of all topics you'll find answers for on this blog here.

Similar to testing our web layer in isolation with @WebMvcTest, Spring Boot provides a convenient way to test our Spring Boot JPA persistence layer: @DataJpaTest. While the default configuration expects an embedded-database, this article demonstrates how to test any Spring Data JPA repository in isolation with Testcontainers and Flyway. The sample application uses Spring Boot 2.4, Java 15, and a PostgreSQL database.

What to test for our Spring Boot JPA Persistence Layer

With Spring Data JPA we can create interfaces for each of our entity classes that extend the JpaRepository interface.

public interface OrderRepository extends JpaRepository<Order, Long> {

Such repositories already provide a set of methods to retrieve, store and delete our JPA entities like .findAll(), .save(), .delete(Order order).

Spring Data also has the concept of derived queries where the actual SQL query is derived from the method name:

public interface OrderRepository extends JpaRepository<Order, Long> {
  List<Order> findAllByTrackingNumber(String trackingNumber);

These methods are validated on startup and if we refer to an unknown column of our entity or don't follow the naming convention, our application won't start. Testing these derived queries and the default methods of the JpaRepository doesn't add many benefits. With such tests, we would rather test the Spring Data framework, which we want to avoid and focus on our application.

Next, what about testing the entity mapping? Should we make sure that our JPA entity model maps to our database schema?

To validate that the mapping is correct, we can rely on a feature of Hibernate that validates the schema on application startup:

spring:
    hibernate:
      ddl-auto: validate

This ensures that for our JPA entities a corresponding database table exists, all columns are present, and they have the correct column type. There are still scenarios that this validation does not cover, e.g. constraints like unique or custom checks.

Hence I wouldn't explicitly focus on testing this, but ensure that all entities are implicitly covered with an integration test that reads from the database and not only from the first-level cache. The TestEntityManager can help here.

So what to focus on when testing our JPA persistence layer? We should focus on non-trivial queries that we handcraft.

A good example might be the following native query that makes use of PostgreSQL's JSON support:

public interface OrderRepository extends JpaRepository<Order, Long> {
  @Query(value = """
      SELECT *
      FROM orders
      WHERE items @> '[{"name": "MacBook Pro"}]';
    """, nativeQuery = true)
  List<Order> findAllContainingMacBookPro();

PS: Wondering how we can create this multi-line string without concatenation? That's the new Java text block feature that is GA since Java 15.

Our Order entity class uses Vlad Mihaleca's hibernate-types project to map PostgreSQL's JSNOB column type to a Java String.

@Entity
@Table(name = "orders")
@TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)
public class Order {
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  @Column(nullable = false, unique = true)
  private String trackingNumber;
  @Type(type = "jsonb")
  @Column(columnDefinition = "jsonb")
  private String items;
  // constructor, getter, setters, etc.

How to test the Spring Boot JPA persistence layer

Knowing what to test brings us to the topic of how to test it.

We could write a unit test. As the Spring Data JPA repositories are interfaces there is not much to verify and test. We need to test these repositories in action. Starting the whole Spring application context for this would be overkill. Spring Boot provides a test annotation that allows us  to start a sliced Spring Context with only relevant JPA components: @DataJpaTest.

By default, this would start an in-memory database like H2 or Derby. As long as our project doesn't use an in-memory database, starting one for our tests is counterproductive. Our Flyway migration scripts might use database-specific features that are not compatible with the in-memory database.

Maintaining a second set of DDL migration scripts for the in-memory database is a non-negligible effort.  In the pre-container times, using an in-memory database might have been the easiest solution as you would otherwise have to install or provide a test database for every developer.

With the rise of Docker, we are now able to start any containerized software on-demand with ease. This includes databases. Testcontainers makes it convenient to start any Docker container of choice for testing purposes. I'm not going to explain Testcontainers in detail here, as there are already several articles available on this blog:

Create the database schema for our @DataJpaTest

An empty database container doesn't help us much with our test case. We need a solution to create our database tables prior to the test execution.

When working with @DataJpaTest and an embedded database we can achieve this with Hibernate's ddl-auto feature set to create-drop. This ensures to first create the database schema based on our Java entity definitions and then drops it afterward.

While this works and might feel convenient (we don't have to write any SQL), we should rather stick to our hand-crafted Flyway migration scripts. Otherwise, there might be a difference between our database schema during the test and production.

Remember: We want to be as close as possible to our production setup when testing our application.

The migration script for our examples contains a single DDL statement:

CREATE TABLE orders (
    ID BIGSERIAL PRIMARY KEY,
    TRACKING_NUMBER VARCHAR(255) UNIQUE NOT NULL,
    ITEMS JSONB

When starting our sliced Spring context for our test, Flyway will first execute our migration scripts whenever our test is working with a fresh and empty database.

Preloading data for the test

Having the database tables created, we now have multiple options to populate data: during the test, using the @Sql annotation, as part of JUnit's @BeforeEach lifecycle or using a custom Docker image.

Let's start with the simplest approach. Every test prepares the data that it needs for verifying a specific method. As the Spring Test Context contains all our Spring Data JPA repositories, we can inject the repository of our choice and save our entities:

orderRepository.save(createOrder("42", "[]"));

Next comes the @Sql annotation. With this annotation new can execute any SQL script prior to test execution. We can place our init scripts inside src/test/resources. It's not necessary to put them on the classpath as we can also reference e.g. a file on disk or an HTTP resource (basically anything that can be resolved by Spring's ResouceUtils class):

INSERT INTO orders (tracking_number, items) VALUES ('42', '[{"name": "MacBook Pro", "amount" : 42}]');
INSERT INTO orders (tracking_number, items) VALUES ('43', '[{"name": "Kindle", "amount" : 13}]');
INSERT INTO orders (tracking_number, items) VALUES ('44', '[]');
@Test
@Sql("/scripts/INIT_THREE_ORDERS.sql")
void shouldReturnOrdersThatContainMacBookPro() {

If all your test cases (in the same test class) share the same data setup,  we can use JUnit Jupiter's @BeforeEach to initialize our tables with data.

@BeforeEach
void initData() {
  orderRepository.save(createOrder("42", "[]"));

We can also create a custom database image that already contains our database with a preset of data. This can also mirror the size of production. For bigger projects we save a lot of time with this approach as Flyway doesn't have to migrate any script. The only downside of this approach is to maintain and keep this test image up-to-date.

Writing the test for our Spring Boot Data JPA repository

Putting it all together, we can now write our test to verify our Spring Boot JPA persistence layer with @DataJpaTest.

As we use Testcontainers to start a PostgreSQL database, we can override the auto-configuration to not use an embedded database:

@DataJpaTest
@Testcontainers
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class OrderRepositoryTest {
  @Container
  static PostgreSQLContainer database = new PostgreSQLContainer("postgres:12")
    .withDatabaseName("springboot")
    .withPassword("springboot")
    .withUsername("springboot");
  @DynamicPropertySource
  static void setDatasourceProperties(DynamicPropertyRegistry propertyRegistry) {
    propertyRegistry.add("spring.datasource.url", database::getJdbcUrl);
    propertyRegistry.add("spring.datasource.password", database::getPassword);
    propertyRegistry.add("spring.datasource.username", database::getUsername);
  @Autowired
  private OrderRepository orderRepository;
  @Test
  void shouldReturnOrdersThatContainMacBookPro() {
    orderRepository.save(createOrder("42", """
         [{"name": "MacBook Pro", "amount" : 42}, {"name": "iPhone Pro", "amount" : 42}]
      """));
    orderRepository.save(createOrder("43", """
         [{"name": "Kindle", "amount" : 13}, {"name": "MacBook Pro", "amount" : 10}]
      """));
    orderRepository.save(createOrder("44", "[]"));
    List<Order> orders = orderRepository.findAllContainingMacBookPro();
    assertEquals(2, orders.size());
  private Order createOrder(String trackingNumber, String items) {
    Order order = new Order();
    order.setTrackingNumber(trackingNumber);
    order.setItems(items);
    return order;

As Testcontainers maps the PostgreSQL port to a random ephemeral port, we can't hardcode the JDBC URL. In the example above we are using @DynamicPropertySource to set all required datasource attributes in a dynamic fashion based on the started container.

We can even write a shorter test with less setup ceremony using Testcontainers JDBC support feature:

@DataJpaTest(properties = {
  "spring.test.database.replace=NONE",
  "spring.datasource.url=jdbc:tc:postgresql:12:///springboot"
class OrderRepositoryShortTest {
  @Autowired
  private OrderRepository orderRepository;
  @Test
  @Sql("/scripts/INIT_THREE_ORDERS.sql")
  void shouldReturnOrdersThatContainMacBookPro() {
    List<Order> orders = orderRepository.findAllContainingMacBookPro();
    assertEquals(2, orders.size());

Note the tc keyword after jdbc for the data source URL. The 12 indicates the PostgreSQL version. With this modification, Testcontainers will start the dockerized PostgreSQL for our test class in the background.

If you want to further optimize this setup when running multiple tests for your persistence layer, you can reuse already started containers with Testcontainers.

Cleanup of the database

The @DataJpaTest meta-annotation contains the @Transactional annotation. This ensures our test execution is wrapped with a transaction that gets rolled-back after the test. This happens for both successful test cases as well as failures.

Hence, there is nothing we have to clean up of our tests and every test starts with empty tables (except we initialize data with our migration scripts).

The source code for this @DataJpaTest example is available on GitHub.

Have fun testing your Spring Boot JPA persistence layer with @DataJpaTest,

Philip

Want to learn more about testing? Join the Testing Java Applications Email Course

  • 14 Days Free E-Mail Course
  • Independent of the application framework
  • All participants receive the JUnit 5 & Mockito Cheat Sheet ($ 9.99)

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK