Spring Boot - Testing a JPA application with @DataJpaTest and Testcontainers
In this section, we will learn how to test Repository layer components with @DataJpaTest and Testcontainers in JPA Spring Boot application that uses PostgreSQL as database.
1. What we will build?
We will create a basic JPA Spring Boot application that uses PostgreSQL as database. We will create Repository layer for this application. Finally we will do a testing with help of Testcontainers to verify our system is working as expected.
2. Testcontainers
Testcontainers is an open source testing library that allows us to run docker containers directly in our spring boot application in order to facilitate integration tests with real dependencies.
It can provide instances of common databases(here PostgreSQL), message brokers, Selenium web browsers, or anything else that can run in a Docker container.
3. @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.
Do you want more information regarding @DataJpaTest? Click here
4. Install docker
Install docker in your local machine if you not yet installed docker. Go to docker official https://docs.docker.com/engine/install/#desktop and download docker and then install.
Complete example
Next we will create a spring boot JPA application, create repository layer which contains three query methods and finally we will do testing with help of Testcontainers to verify our system is working as expected.
5. Creating a 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-testcontainers-example. Here I selected the Maven project - language Java 17 - Spring Boot 3.1.6 , Spring Data JPA, Testcontainers, 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-testcontainers-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.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.knf.dev.demo</groupId>
<artifactId>datajpatest-testcontainers-example</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>datajpatest-testcontainers-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>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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</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-testcontainers:By using this dependency spring automatically configures the necessary Spring Boot properties for the supporting containers.
junit-jupiter testcontainers extension which will take care of starting and stopping of the containers.
spring-boot-starter-test starter will provide following libraries:
- JUnit
- Spring Test & Spring Boot Test
- AssertJ
- Hamcrest
- Mockito
- JSONassert
- JsonPath
application.yaml
*For our testing purposes there is no significance for the following configuration*
If you are running the application with real postgresql database, then configure Spring Boot to use PostgreSQL as data source. We are simply adding PostgreSQL database URL, username, and password in the src/main/resources/application.yaml.
#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
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 script for loading data.
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
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> findStudentByAgeGreaterThan(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.
- findStudentByAgeGreaterThan(): 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.
Application.java
package com.knf.dev.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Implementing the Tests
When using JUnit 4, @SpringBootTest 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.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.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.test.context.jdbc.Sql;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
@DataJpaTest
@Testcontainers
@AutoConfigureTestDatabase(replace= AutoConfigureTestDatabase.Replace.NONE)
@Sql({"/test-student-data.sql"})
public class StudentRepositoryTests {
@Autowired
private StudentRepository studentRepository;
@Container
@ServiceConnection
public static PostgreSQLContainer postgreSQLContainer =
new PostgreSQLContainer<>(DockerImageName.parse("postgres:latest"));
@Test
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
void findByAgeGreaterThan_ReturnsTheListStudents() {
List<Student> students = studentRepository.findStudentByAgeGreaterThan(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
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));
}
}
- @Testcontainers is a JUnit Jupiter extension which automatically starts and stops the containers(here PostgreSQLContainer) that are used in the tests.
- The @Container annotation is a JUnit extension that tells JUnit to notify this field about various events in a test lifecycle. In this case, it ensures that the PostgreSQL container is running in a healthy way before any test is considered successfully run.
- Beginning from Spring Boot version 3.1, the annotation @ServiceConnection can be used on the container instance fields of our tests. We are using @ServiceConnection instead of @DynamicPropertySource to register the dynamic property values to a PostgreSQL container.
- Testcontainers has a PostgreSQLContainer class that enables you to create a PostgreSQL instance for testing purposes.
- 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.
6. Run the test
Since we are using Testcontainers , make sure to start Docker in your local machine.
After that, run the test,
Or you can run the test using following command:
mvn test -Dtest=StudentRepositoryTests
or
mvn test