Spring Boot - Testing a JPA application With @DataJpaTest - Example

In this section, we will learn how to test Repository layer components with @DataJpaTest in Spring Boot application.

1. @DataJpaTest 

Instead of bootstrapping the entire application context for every test, @DataJpaTest allows us to initialize only the parts of the Application context that are relevant to JPA tests.

By default, it scans for @Entity classes and configures Spring Data JPA repositories. If an embedded database is available on the classpath, @DataJpaTest will autoconfigure one for testing purposes.

By default, tests annotated with @DataJpaTest are transactional and roll back at the end of each test, means we do not need to clean up saved or modified table data after each test.

Regular @Component, @Service or @Controller beans are not scanned when using this annotation. 

This approach not only speeds up the testing process but also ensures a focused and efficient testing environment. 

This approach is also known as "slicing" the application context.

Find the sample code snippet to use @DataJpaTest annotation in unit test class. 

@DataJpaTest
public class StudentRepositoryTests {

@Autowired
private StudentRepository studentRepository;

@Test
void findByName_ReturnsTheStudent() {

//todo...
}
}

When using JUnit 4, this annotation should be used in combination with @RunWith(SpringRunner.class). But for this example  we are using JUnit 5, there’s no need to add the equivalent @ExtendWith(SpringExtension.class).


2. Transactions

By default, tests annotated with @DataJpaTest are transactional and roll back at the end of each test. We can disable transaction management for a test, use Propagation.NOT_SUPPORTED.

@DataJpaTest
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public class StudentRepositoryTests {

@Autowired
private StudentRepository studentRepository;

@Test
void findByName_ReturnsTheStudent() {

// todo...
}

}


3. Test Against a Real Database

If you want run the tests against an application configured real database, use Replace.NONE.

@DataJpaTest
@AutoConfigureTestDatabase(replace= AutoConfigureTestDatabase.Replace.NONE)//Testing with real database
public class RealDbStudentRepositoryTests {

@Autowired
private StudentRepository studentRepository;

@Test
void findByName_ReturnsTheStudent() {

//todo...
}
}


Complete example

Next we will create a spring boot JPA application, create repository layer which contains three query methods and finally we will write test against H2 in-memory database and Real(PostgreSQL) database.

4. Creating spring boot application

First, open the Spring initializr https://start.spring.io

Then, Provide the Group and Artifact name. We have provided Group name com.knf.dev.demo and Artifact datajpatest-example. Here I selected the Maven project - language Java 17 - Spring Boot 3.1.5 , Spring Data JPA, H2 Database, and PostgreSQL Driver.

Then, click on the Generate button. When we click on the Generate button, it starts packing the project in a .zip(datajpatest-example) file and downloads the project. Then, Extract the Zip file. 

Then, import the project on your favourite IDE.

Final Project directory:


Complete pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<
modelVersion>4.0.0</modelVersion>
<
parent>
<
groupId>org.springframework.boot</groupId>
<
artifactId>spring-boot-starter-parent</artifactId>
<
version>3.1.5</version>
<
relativePath/> <!-- lookup parent from repository -->
</parent>
<
groupId>com.knf.dev.demo</groupId>
<
artifactId>datajpatest-example</artifactId>
<
version>0.0.1-SNAPSHOT</version>
<
name>datajpatest-example</name>
<
description>Demo project for Spring Boot</description>
<
properties>
<
java.version>17</java.version>
</
properties>
<
dependencies>
<
dependency>
<
groupId>org.springframework.boot</groupId>
<
artifactId>spring-boot-starter-data-jpa</artifactId>
</
dependency>

<
dependency>
<
groupId>com.h2database</groupId>
<
artifactId>h2</artifactId>
<scope>test</scope>
</
dependency>
<
dependency>
<
groupId>org.postgresql</groupId>
<
artifactId>postgresql</artifactId>
<scope>runtime</scope>
</
dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

</
dependencies>

<
build>
<
plugins>
<
plugin>
<
groupId>org.springframework.boot</groupId>
<
artifactId>spring-boot-maven-plugin</artifactId>
<
configuration>
<
image>
<
builder>paketobuildpacks/builder-jammy-base:latest</builder>
</
image>
</
configuration>
</
plugin>
</
plugins>
</
build>

</
project>

spring-boot-starter-test starter will provide following libraries:

  • JUnit 
  • Spring Test & Spring Boot Test 
  • AssertJ
  • Hamcrest 
  • Mockito 
  • JSONassert 
  • JsonPath 


application.yaml

We are writing test against H2 in-memory database as well as Real(Postgresql) database. 
So, Let’s configure Spring Boot to use PostgreSQL as our data source. We are simply adding PostgreSQL database URL, username, and password in the src/main/resources/application.yaml
Spring Boot doesn’t require any special configuration for h2 database to work.

#Real database(PostgreSQL) configuration
spring:
datasource:
url: jdbc:postgresql://localhost:5432/postgres
username: postgres
password: root
jpa:
hibernate:
ddl-auto: update # Hibernate ddl auto (create, create-drop, validate, update),production set to none or comment it
show-sql: true
open-in-view: false
generate-ddl: true


#Spring Boot doesn’t require any special configuration for h2 database to work.


Spring Data JPA – Student Entity

A Student object as JPA entity.

package com.knf.dev.demo.entity;

import jakarta.persistence.*;

@Entity
@Table(name = "students")
public class Student {

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

private String name;

private String email;

private Integer age;

public Student() {
}

public Student(String name, String email, Integer age) {
this.name = name;
this.email = email;
this.age = age;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}
}

The @Entity annotation specifies that the class is an entity and is mapped to a database table. The @Table annotation specifies the name of the database table to be used for mapping. The @Id annotation specifies the primary key of an entity and the @GeneratedValue provides for the specification of generation strategies for the values of primary keys.


Spring Data JPA – Student Repository

It doesn't make sense to test inherited default methods like save(), findById(), deleteById(), or findAll() from JpaRepository. If we are doing like so, means we are testing the framework.

So, i created three methods getStudentsByAge() findByName() , and findByAgeLessThan() 

package com.knf.dev.demo.repository;

import com.knf.dev.demo.entity.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;
import java.util.Optional;

public interface StudentRepository extends JpaRepository<Student,Long> {

//Using JPQL query
@Query("FROM Student WHERE age > ?1")
List<Student> getStudentsByAge(Integer age);

//Using native sql query
@Query(value = "select * from students as u where u.name = :name",
nativeQuery = true)
Optional<Student> findByName(@Param("name") String name);

//Derived Query Method
List<Student> findByAgeLessThan(Integer age);

}

  • JpaRepository is a JPA-specific extension of Repository. It contains an API for basic CRUD operations and also API for pagination and sorting.
  • getStudentsByAge(): If we want to retrieve students whose age is greater than the given age.
  • findByName(): This method will get student entity by name.(Optional)
  • findByAgeLessThan(): If we want to retrieve students whose age is less than the given age.

Loading Initial Data 

Create test-student-data.sql

INSERT INTO students (id,name, email, age) VALUES
(101,'Alpha', 'alpha@knf.com', 50),
(102,'Beta', 'beta@knf.com', 40),
(103,'Gama', 'gama@knf.com', 30),
(104,'Pekka', 'pekka@knf.com', 20);

Later, as the part of testing we will use this file for loading data.


Create test-student-schema.sql(For H2 DB)

CREATE TABLE students (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(250) NOT NULL,
email VARCHAR(250) NOT NULL,
age INT
);

Later, we will use this file while writing test.


DataJpaTestExampleApplication.java

package com.knf.dev.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DataJpaTestExampleApplication {

public static void main(String[] args) {
SpringApplication.run(DataJpaTestExampleApplication.class, args);
}
}


Write unit test against H2 in-memory database

When using JUnit 4, this annotation should be used in combination with @RunWith(SpringRunner.class). But for this example  we are using JUnit 5, there’s no need to add the equivalent @ExtendWith(SpringExtension.class).

package com.knf.dev.demo;


import com.knf.dev.demo.entity.Student;
import com.knf.dev.demo.repository.StudentRepository;
import org.assertj.core.api.AssertionsForInterfaceTypes;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.jdbc.Sql;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;


@DataJpaTest
@Sql("/test-student-schema.sql")
public class StudentRepositoryTests {

@Autowired
private StudentRepository studentRepository;

@Test
@Sql({"/test-student-data.sql"})
void findByName_ReturnsTheStudent() {

Student student = studentRepository.findByName("Alpha").get();
assertThat(student).isNotNull();
assertThat(student.getEmail()).isEqualTo("alpha@knf.com");
assertThat(student.getName()).isEqualTo("Alpha");
assertThat(student.getId()).isEqualTo(101);
assertThat(student.getAge()).isEqualTo(50);
}

@Test
@Sql({"/test-student-data.sql"})
void findByAgeGreaterThan_ReturnsTheListStudents() {

List<Student> students = studentRepository.getStudentsByAge(29);

//Convert list of students to list of id(Integer)
List<Integer> ids = students.stream()
.map(o -> o.getId().intValue())
.collect(Collectors.toList());

assertThat(students.size()).isEqualTo(3);
assertThat(ids).hasSameElementsAs(Arrays.asList(103,102,101));
}

@Test
@Sql({"/test-student-data.sql"})
void findByAgeLessThan_ReturnsTheListStudents() {

List<Student> students = studentRepository.findByAgeLessThan(31);

//Convert list of students to list of id(Integer)
List<Integer> ids = students.stream()
.map(o -> o.getId().intValue())
.collect(Collectors.toList());

assertThat(students.size()).isEqualTo(2);
assertThat(ids).hasSameElementsAs(Arrays.asList(104,103));
}
}

  • The @Sql annotation executes SQL scripts and SQL statements using datasource for testing.
  • assertThat is used to check the specified value matches the expected value. It will accept the two parameters, the first contains the actual value, and the second will have the object matching the condition.


Write unit test against Real(PostgreSQL) database

package com.knf.dev.demo;


import com.knf.dev.demo.entity.Student;
import com.knf.dev.demo.repository.StudentRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.jdbc.Sql;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;

@DataJpaTest
@AutoConfigureTestDatabase(replace= AutoConfigureTestDatabase.Replace.NONE)//Testing with real database
public class RealDbStudentRepositoryTests {

@Autowired
private StudentRepository studentRepository;

@Test
@Sql({"/test-student-data.sql"})
void findByName_ReturnsTheStudent() {

Student student = studentRepository.findByName("Alpha").get();
assertThat(student).isNotNull();
assertThat(student.getEmail()).isEqualTo("alpha@knf.com");
assertThat(student.getName()).isEqualTo("Alpha");
assertThat(student.getId()).isEqualTo(101);
assertThat(student.getAge()).isEqualTo(50);
}

@Test
@Sql({"/test-student-data.sql"})
void findByAgeGreaterThan_ReturnsTheListStudents() {

List<Student> students = studentRepository.getStudentsByAge(29);

//Convert list of students to list of id(Integer)
List<Integer> ids = students.stream()
.map(o -> o.getId().intValue())
.collect(Collectors.toList());

assertThat(students.size()).isEqualTo(3);
assertThat(ids).hasSameElementsAs(Arrays.asList(103,102,101));
}

@Test
@Sql({"/test-student-data.sql"})
void findByAgeLessThan_ReturnsTheListStudents() {

List<Student> students = studentRepository.findByAgeLessThan(31);

//Convert list of students to list of id(Integer)
List<Integer> ids = students.stream()
.map(o -> o.getId().intValue())
.collect(Collectors.toList());

assertThat(students.size()).isEqualTo(2);
assertThat(ids).hasSameElementsAs(Arrays.asList(104,103));
}
}


5. Run the test

Or you can run the test using following command:

mvn  test -Dtest=StudentRepositoryTests
mvn  test -Dtest=RealDbStudentRepositoryTests

Popular posts from this blog

Learn Java 8 streams with an example - print odd/even numbers from Array and List

Java Stream API - How to convert List of objects to another List of objects using Java streams?

Registration and Login with Spring Boot + Spring Security + Thymeleaf

Java, Spring Boot Mini Project - Library Management System - Download

ReactJS, Spring Boot JWT Authentication Example

Spring Boot + Mockito simple application with 100% code coverage

Top 5 Java ORM tools - 2024

Java - Blowfish Encryption and decryption Example

Spring boot video streaming example-HTML5

Google Cloud Storage + Spring Boot - File Upload, Download, and Delete