Spring Boot PostgreSQL - Integration Testing with Testcontainers - Example

In this section, we will learn how to test Spring Boot, Spring Data JPA, and PostgreSQL based application using Testcontainers.

1. What we will build?

We will create a web application with Spring Boot, Spring Data JPA and PostgreSQL database. The application will consist of three layer,  ie. a Controller, a Service, and a Repository layer. 

  • Controller layer takes care of mapping request data to the defined request handler method. Once response body is generated from the handler method, it converts it to JSON response.
  • Service layer  facilitates communication between the controller and the repository layer.
  • We are using Spring Data JPA for managing database operations, so we can use Spring Data JPARepository interface.

**Finally we will do a integration 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. 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.


4. 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 spring-boot-postgresql-testcontainers-example. Here I selected the Maven project - language Java 17 - Spring Boot 3.1.5 , Spring Web, Testcontainers, Spring Data JPA and PostgreSQL Driver.

Then, click on the Generate button. When we click on the Generate button, it starts packing the project in a .zip(spring-boot-postgresql-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.5</version>
<
relativePath/> <!-- lookup parent from repository -->
</parent>
<
groupId>com.knf.dev.demo</groupId>
<
artifactId>spring-boot-postgresql-testcontainers-example</artifactId>
<
version>0.0.1-SNAPSHOT</version>
<
name>spring-boot-postgresql-testcontainers-example</name>
<
description>Spring Boot and PostgreSQL Integration testing with Tescontainers </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.springframework.boot</groupId>
<
artifactId>spring-boot-starter-web</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.


Create clear-data.sql

DELETE FROM students;

Later, as the part of testing we will use this script for clearing table.


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(Long id, String name, String email, Integer age) {
this.id = id;
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.


Create StudentService.java 

package com.knf.dev.demo.service;

import com.knf.dev.demo.entity.Student;

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

public interface StudentService {

List<Student> findStudentByAgeGreaterThan(Integer age);
Student findByName(String name);
List<Student> findByAgeLessThan(Integer age);
}


Create StudentServiceImpl.java 

package com.knf.dev.demo.service;

import com.knf.dev.demo.entity.Student;
import com.knf.dev.demo.repository.StudentRepository;
import org.springframework.stereotype.Service;

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

@Service
public class StudentServiceImpl implements StudentService{

private final StudentRepository studentRepository;

public StudentServiceImpl(StudentRepository studentRepository) {
this.studentRepository = studentRepository;
}

@Override
public List<Student> findStudentByAgeGreaterThan(Integer age) {
return studentRepository.findStudentByAgeGreaterThan(age);
}

@Override
public Student findByName(String name) {
return studentRepository.findByName(name).get();
}

@Override
public List<Student> findByAgeLessThan(Integer age) {
return studentRepository.findByAgeLessThan(age);
}

}


Create Student Controller

package com.knf.dev.demo.controller;

import com.knf.dev.demo.entity.Student;
import com.knf.dev.demo.service.StudentService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/api/v1")
public class StudentController {

private final StudentService studentService;

public StudentController(StudentService studentService) {
this.studentService = studentService;
}

@GetMapping("/students/agt/{age}")
public List<Student> findStudentByAgeGreaterThan(@PathVariable Integer age)
{
return studentService.findStudentByAgeGreaterThan(age);
}

@GetMapping("/students/alt/{age}")
public List<Student> findStudentByAgeLessThan(@PathVariable Integer age)
{
return studentService.findByAgeLessThan(age);
}

@GetMapping("/students/{name}")
public Student findStudentByName(@PathVariable String name)
{
return studentService.findByName(name);
}
}

The @RestController annotation is mainly utilized for building restful web services utilizing Spring MVC. It is a convenience annotation, this annotation itself annotated with @ResponseBody and @Controller annotation. The class annotated with @RestController annotation returns JSON replication in all the methods. 

@RequestMapping is used to map web requests onto specific handler classes and/or handler methods. @RequestMapping can be applied to the controller class as well as methods.

@GetMapping annotation for mapping HTTP GET requests onto specific handler methods.


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.service.StudentService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.web.client.RestTemplate;
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;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
public class StudentApplicationIntegrationTest {

@Autowired
private StudentService studentService;

@LocalServerPort
private Integer port;

@Container
@ServiceConnection
public static PostgreSQLContainer postgreSQLContainer =
new PostgreSQLContainer<>(DockerImageName.parse("postgres:latest"));


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

final Integer age = 29;

RestTemplate restTemplate = new RestTemplate();

String resourceUrl= "http://localhost:"+port+"/api/v1/students/agt/{age}";

// Fetch response as List wrapped in ResponseEntity
ResponseEntity<List<Student>> findByAgeGreaterThan = restTemplate.exchange(
resourceUrl,
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<Student>>(){},age);
List<Student> students = findByAgeGreaterThan.getBody();

//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({"/clear-data.sql","/test-student-data.sql"})
void findByName_ReturnsTheStudent() {

final String name ="Alpha";

RestTemplate restTemplate = new RestTemplate();

String resourceUrl= "http://localhost:"+port+"/api/v1/students/{name}";

// Fetch response as List wrapped in ResponseEntity
ResponseEntity<Student> findByName = restTemplate.exchange(
resourceUrl,
HttpMethod.GET,
null,
new ParameterizedTypeReference<Student>(){},name);

Student student = findByName.getBody();


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({"/clear-data.sql","/test-student-data.sql"})
void findByAgeLessThan_ReturnsTheListStudents() {

final Integer age = 31;

RestTemplate restTemplate = new RestTemplate();

String resourceUrl= "http://localhost:"+port+"/api/v1/students/alt/{age}";

// Fetch response as List wrapped in ResponseEntity
ResponseEntity<List<Student>> findByAgeLessThan = restTemplate.exchange(
resourceUrl,
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<Student>>(){},age);
List<Student> students = findByAgeLessThan.getBody();

//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));
}
}

  • @SpringBootTest can be used to load complete application context for end to end integration testing.
  • webEnvironment=SpringBootTestWebEnvironment.RANDOM_PORT: Loads WebApplicationContext and provides a real web environment. Embedded servers start and listen on a random port.
  • @Testcontainers is a JUnit Jupiter extension which automatically starts and stops the containers(here PostgreSQLContainer) that are used in the tests.
  • To get the port number on which your Spring Boot application is running, you can use the @LocalServerPort annotation.
  • 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.


5. 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=StudentApplicationIntegrationTest
or
mvn test

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