Boot

DB Integration Tests with Spring Boot and Testcontainers

Hello. In this tutorial, we will explore the Testcontainers to perform integration tests for the jpa repositories in a Spring Boot app.

You can also check this tutorial in the following video:

Spring Boot and Testcontainers – Video

1. Introduction

Testcontainers is a library that provides a clean way for writing the integration and end-to-end tests for the jpa repositories. In this example, we will create a simple rest api application to persist the data in the database and make use of the Postgres test container to have the jpa testing. For test containers to work ensure to have the Docker up and running on your machine. If someone needs to go through the Docker installation, please watch this video.

2. DB Integration Tests with Spring Boot and Testcontainers

Let us dive into some practice stuff and I am hoping that you are aware of the spring boot basics.

2.1 Tools Used for Spring boot application and Project Structure

We are using Eclipse Kepler SR2, JDK 8, and Maven. In case you’re confused about where you should create the corresponding files or folder, let us review the project structure of the spring boot application.

Spring Boot Testcontainers - project structure
Fig. 1: Project structure

Let us start building the application!

3. Creating a Spring Boot application

Below are the steps involved in developing the application.

3.1 Maven Dependency

In the pom.xml file we will define the required dependencies.

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns="http://maven.apache.org/POM/4.0.0"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <artifactId>SpringbootTestcontainers</artifactId>

  <build>
    <plugins>
      <plugin>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
          <excludes>
            <exclude>
              <artifactId>lombok</artifactId>
              <groupId>org.projectlombok</groupId>
            </exclude>
          </excludes>
        </configuration>
        <groupId>org.springframework.boot</groupId>
      </plugin>
    </plugins>
  </build>

  <dependencies>
    <dependency>
      <artifactId>spring-boot-starter-data-jpa</artifactId>
      <groupId>org.springframework.boot</groupId>
    </dependency>
    <dependency>
      <artifactId>spring-boot-starter-web</artifactId>
      <groupId>org.springframework.boot</groupId>
    </dependency>

    <dependency>
      <groupId>com.github.javafaker</groupId>
      <artifactId>javafaker</artifactId>
      <version>1.0.2</version>
    </dependency>

    <dependency>
      <artifactId>postgresql</artifactId>
      <groupId>org.postgresql</groupId>
      <scope>runtime</scope>
    </dependency>

    <dependency>
      <artifactId>lombok</artifactId>
      <groupId>org.projectlombok</groupId>
      <optional>true</optional>
    </dependency>

    <dependency>
      <artifactId>spring-boot-starter-test</artifactId>
      <groupId>org.springframework.boot</groupId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <artifactId>junit-jupiter</artifactId>
      <groupId>org.testcontainers</groupId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <artifactId>postgresql</artifactId>
      <groupId>org.testcontainers</groupId>
      <scope>test</scope>
    </dependency>
  </dependencies>
  <dependencyManagement>
    <dependencies>
      <dependency>
        <artifactId>testcontainers-bom</artifactId>
        <groupId>org.testcontainers</groupId>
        <scope>import</scope>
        <type>pom</type>
        <version>${testcontainers.version}</version>
      </dependency>
    </dependencies>
  </dependencyManagement>
  <description>Demo project for Spring Boot and Testcontainers</description>

  <groupId>com.springboot</groupId>
  <modelVersion>4.0.0</modelVersion>

  <name>SpringbootTestcontainers</name>

  <parent>
    <artifactId>spring-boot-starter-parent</artifactId>
    <groupId>org.springframework.boot</groupId>
    <relativePath/>
    <version>2.5.6</version> <!-- lookup parent from repository -->
  </parent>

  <properties>
    <java.version>1.8</java.version>
    <testcontainers.version>1.16.0</testcontainers.version>
  </properties>

  <version>0.0.1-SNAPSHOT</version>

</project>

3.2 Application properties file

Create a properties file in the resources folder and add the following content to it. The file will contain information about the database connectivity and spring jpa. For this tutorial, we will use the Postgresql database. I already have the container up and running on the localhost:5433.

application.properties

server.port=9800
spring.application.name=springboot-and-testcontainers
#database settings
spring.datasource.username=your_db_username
spring.datasource.password=your_db_password
##sample url - jdbc:postgresql://hostname:port/your_db_name
spring.datasource.url=your_db_url
spring.datasource.driver-class-name=org.postgresql.Driver
#jpa settings
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=false

3.3 Java Classes

Let us write the important java class(es) involved in this tutorial. The other non-important classes for this tutorial like the main, controller, service, exceptions, and bootstrap can be downloaded from the Downloads section.

3.3.1 Model class

Create a model class that will be responsible for schema and data in the sql table.

Book.java

package com.springboot.model;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;

// entity table.

//lombok
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
//spring
@Entity
@Table(name = "book")
@Component
public class Book {

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  int id;
  String author;
  String title;
  String genre;
  String publisher;
  int quantity;
}

3.3.2 Repository interface

Add the following code to the repository interface that extends the JpaRepository. The interface consists of custom methods to clearly understand the jpa and test container implementation.

BookRepository.java

package com.springboot.repository;

import com.springboot.model.Book;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface BookRepository extends JpaRepository<Book, Integer> {

	// custom jpa method to find books by genre.
	List<Book> findBooksByGenre(String genre);

	// custom jpa method to find books by quantity.
	List<Book> findBooksByQuantityGreaterThanEqual(int quantity);

	// custom jpa method to find a book by name.
	Book findFirstByTitle(String title);
}

3.4 Test cases implementation

To set up the test container in this tutorial we will need Docker (for pulling the image used by the test container) and the required testcontainer library. The dependency is already added to the pom.xml file.

3.4.1 Starting and stopping the container

Spring boot provides a feature called the slice test which is a neat way to test the horizontal slices of the application. To test the jpa we will make use of the Book repository interface created above. Now to configure a database that is exclusively available for our tests we will create a BaseIT class in the test folder.

BaseIT.java

package com.springboot.repository;

import org.testcontainers.containers.PostgreSQLContainer;

// using the singleton container approach to improve the performance of our tests.
public abstract class BaseIT {

  static PostgreSQLContainer<?> container;

  static {
    container = new PostgreSQLContainer<>("postgres:alpine")
        .withUsername("duke")
        .withPassword("password")
        .withDatabaseName("container")
        .withReuse(true);

    container.start();
  }
}

3.4.2 Running the tests

With this done we can write our tests. We now will create a class named BookRepositoryTest.java that will extend the BaseIT class. The class will consist of the test cases which we can run as Junit tests.

BookRepositoryTest.java

package com.springboot.repository;

import static org.assertj.core.api.Assertions.assertThat;
import static org.testcontainers.shaded.org.apache.commons.lang.RandomStringUtils.randomAlphabetic;

import com.github.javafaker.Faker;
import com.springboot.model.Book;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
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.jdbc.AutoConfigureTestDatabase.Replace;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

// annotation is used to test the jpa repositories
// by default uses the embedded in-memory database for testing
@DataJpaTest
// annotation used to configure a test database instead of application
// defined or auto-configured datasource
@AutoConfigureTestDatabase(replace = Replace.NONE)
class BookRepositoryTest extends BaseIT {

  private static final Faker FAKER = new Faker(Locale.ENGLISH);

  @Autowired
  BookRepository objUnderTest;

  @Test
  void shouldFindBookById() {
    Book actual = create(randomAlphabetic(5), randomAlphabetic(5), 1);
    objUnderTest.saveAndFlush(actual);

    Book expected = objUnderTest.findById(actual.getId()).get();
    assertThat(expected).usingRecursiveComparison().isEqualTo(actual);
  }

  @Test
  void shouldFindBooksByGenre() {
    String genre = "Fable";
    List<Book> actual = prepare(2, randomAlphabetic(5), genre, 10);
    objUnderTest.saveAllAndFlush(actual);

    List<Book> expected = objUnderTest.findBooksByGenre(genre);
    assertThat(expected).usingRecursiveComparison().isEqualTo(actual);
  }

  @Test
  void shouldFindBooksByGenre_ReturnAnEmptyList() {
    List<Book> actual = prepare(2, randomAlphabetic(2), "Fiction", 1);
    objUnderTest.saveAllAndFlush(actual);

    assertThat(objUnderTest.findBooksByGenre(randomAlphabetic(5))).isEmpty();
  }

  @Test
  void shouldFindBooksByQuantity() {
    int quantity = 60;
    List<Book> actual = prepare(5, randomAlphabetic(5), randomAlphabetic(5), quantity);
    objUnderTest.saveAllAndFlush(actual);

    List<Book> expected = objUnderTest.findBooksByQuantityGreaterThanEqual(quantity);
    assertThat(expected).usingRecursiveComparison().isEqualTo(actual);
  }

  @Test
  void shouldFindBooksByQuantity_ReturnAnEmptyList() {
    List<Book> actual = prepare(2, randomAlphabetic(2), randomAlphabetic(5), 3);
    objUnderTest.saveAllAndFlush(actual);

    assertThat(objUnderTest.findBooksByQuantityGreaterThanEqual(50)).isEmpty();
  }

  @Test
  void shouldFindFirstBookByTitle() {
    Book book1 = create("Harry Potter", "Fantasy Fiction", 5);
    Book book2 = create("Harry Potter", "Fantasy Fiction", 10);
    List<Book> actual = Arrays.asList(book1, book2);
    objUnderTest.saveAllAndFlush(actual);

    assertThat(objUnderTest.findAll().size()).isEqualTo(2);

    Book expected = objUnderTest.findFirstByTitle("Harry Potter");
    assertThat(expected).usingRecursiveComparison().isEqualTo(book1);
  }

  //helper methods.

  private List<Book> prepare(int iterations, String title, String genre, int quantity) {
    List<Book> books = new ArrayList<>();
    for (int i = 0; i < iterations; i++) {
      books.add(create(title, genre, quantity));
    }
    return books;
  }

  private Book create(String title, String genre, int quantity) {
    return Book.builder()
        .author(FAKER.book().author())
        .title(title)
        .genre(genre)
        .publisher(FAKER.book().publisher())
        .quantity(quantity)
        .build();
  }
}

4. Run the Testcases

To execute the repository test cases, right-click on the BookRepositoryTest.java class, Run As -> Junit Tests. If everything goes well the test cases will be passed successfully as shown in Fig. 2. The test container will download the given docker image to run the repository test cases.

Spring Boot Testcontainers - running the test cases
Fig. 2: Running the test cases

5. Project Demo

Run the implementation file (i.e. TestContainersApp.java). To test the application endpoints we will use the postman tool. However, you’re free to use any tool of your choice for interacting with the application endpoints.

Application endpoints

-- get a book by id -- 
http://localhost:9800/book/id?key=1

-- get books --
http://localhost:9800/book/all

-- get books by genre --
http://localhost:9800/book/genre?type=Fable

-- get books by quantity --
http://localhost:9800/book/quantity?quantity=5

That is all for this tutorial and I hope the article served you whatever you were looking for. Happy Learning and do not forget to share!

6. Summary

In this tutorial, we learned the implementation of test containers to test the repository self or custom methods. You can download the sample application as an Eclipse project in the Downloads section.

7. Download the Project

This was an example of test containers implementation in a sping application to test the repository methods.

Download
You can download the full source code of this example here: DB Integration Tests with Spring Boot and Testcontainers

Yatin

An experience full-stack engineer well versed with Core Java, Spring/Springboot, MVC, Security, AOP, Frontend (Angular & React), and cloud technologies (such as AWS, GCP, Jenkins, Docker, K8).
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button