Christopher Anabo
Christopher Anabo
Senior Tech Lead
Christopher Anabo

Notes

Testcontainers in a Java Spring Boot Project

Testcontainers in a Java Spring Boot Project

We are going to create a Spring Boot project using Spring Data JPA together with Postgres and implement a REST API endpoint to return all the customer details that are stored in the database. Then we will test this API using the Testcontainers Postgres module and RestAssured.

Getting Started

You can create a new Spring Boot project from Spring Initializr by selecting the Spring WebSpring Data JPAPostgreSQL Driver and Testcontainers starters.

Instead, you can clone https://github.com/anddegs/test-containers-postgres.git repository and switch to the initial branch.

If you have selected the Maven build tool you can see that the following Spring Boot starters as well as the Testcontainers Postgres module dependencies are added to the pom.xml.

<properties>
    <java.version>17</java.version>
    <testcontainers.version>1.19.8</testcontainers.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.testcontainers</groupId>
        <artifactId>junit-jupiter</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>postgresql</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

Create JPA entity

First let us start with creating a JPA entity Customer.java.

package com.testcontainers.demo;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "customers")
class Customer {

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

  @Column(nullable = false)
  private String name;

  @Column(nullable = false, unique = true)
  private String email;

  public Customer() {}

  public Customer(Long id, String name, String email) {
    this.id = id;
    this.name = name;
    this.email = email;
  }

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

Create Spring Data JPA repository

Spring Data JPA is an abstraction on top of JPA and provides basic CRUD operations, sorting and pagination capabilities and dynamic query generation from method names.

Let us create a Spring Data JPA repository interface for the Customer entity.

package com.testcontainers.demo;

import org.springframework.data.jpa.repository.JpaRepository;

interface CustomerRepository extends JpaRepository {}

Add schema creation script

As we are not using any in-memory database, we need to create the Postgres database tables by some means. The recommended approach is to use some database migration tool like Flyway or Liquibase, but for this guide we will use simple schema initialization support provided by Spring Boot.

Create a schema.sql file with the following content under the src/main/resources directory.

create table if not exists customers (
    id bigserial not null,
    name varchar not null,
    email varchar not null,
    primary key (id),
    UNIQUE (email)
);

We also need to enable schema initialization by adding the following property in the src/main/resources/application.properties file. 

spring.sql.init.mode=always

Create REST API endpoint

Finally, create a controller to implement a REST API endpoint to fetch all customers from the database.

package com.testcontainers.demo;

import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
class CustomerController {

  private final CustomerRepository repo;

  CustomerController(CustomerRepository repo) {
    this.repo = repo;
  }

  @GetMapping("/api/customers")
  List getAll() {
    return repo.findAll();
  }
}

Write test for API endpoint

We are going to write a test for the REST API GET /api/customers endpoint by starting the Spring context using the @SpringBootTest annotation and invoke the APIs using RestAssured.

First let us add the rest-assured library dependency.

If you are using Maven then add the following dependency in pom.xml file.

<dependency>
    <groupId>io.rest-assured</groupId>
    <artifactId>rest-assured</artifactId>
    <scope>test</scope>
</dependency>


 

But in order to successfully start our Spring context we need a Postgres database up and running and configure the context to talk to that database. This is where Testcontainers comes into the picture.

We can use the Testcontainers library to spin up a Postgres database instance as a Docker container and configure the application to talk to that database as follows:

package com.testcontainers.demo;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.hasSize;

import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import java.util.List;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
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.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class CustomerControllerTest {

  @LocalServerPort
  private Integer port;

  static PostgreSQLContainer postgres = new PostgreSQLContainer<>(
    "postgres:16-alpine"
  );

  @BeforeAll
  static void beforeAll() {
    postgres.start();
  }

  @AfterAll
  static void afterAll() {
    postgres.stop();
  }

  @DynamicPropertySource
  static void configureProperties(DynamicPropertyRegistry registry) {
    registry.add("spring.datasource.url", postgres::getJdbcUrl);
    registry.add("spring.datasource.username", postgres::getUsername);
    registry.add("spring.datasource.password", postgres::getPassword);
  }

  @Autowired
  CustomerRepository customerRepository;

  @BeforeEach
  void setUp() {
    RestAssured.baseURI = "http://localhost:" + port;
    customerRepository.deleteAll();
  }

  @Test
  void shouldGetAllCustomers() {
    List customers = List.of(
      new Customer(null, "John", "john@mail.com"),
      new Customer(null, "Dennis", "dennis@mail.com")
    );
    customerRepository.saveAll(customers);

    given()
      .contentType(ContentType.JSON)
      .when()
      .get("/api/customers")
      .then()
      .statusCode(200)
      .body(".", hasSize(2));
  }
}

Let us understand what is going on in this test.

  • We have annotated the test class with the @SpringBootTest annotation together with the webEnvironment config, so that the test will run by starting the entire application on a random available port.

  • We have created an instance of PostgreSQLContainer using the postgres:16-alpine Docker image. The Postgres container is started using JUnit 5 @BeforeAll callback method which gets executed before running any test method within a test instance.

  • The Postgres database runs on port 5432 inside the container and maps to a random available port on the host.

  • We have registered the database connection properties dynamically obtained from the Postgres container using Spring Boot’s DynamicPropertyRegistry.

  • We have injected the random port on which the Spring Boot application started using @LocalServerPort and registered the RestAssured baseURI.

  • We are deleting all customer rows using JUnit 5 @BeforeEach callback method which gets executed before every test method. This will ensure the predictable data setup for every test and circumvent any kind of test pollution.

  • Finally, in the shouldGetAllCustomers() test, we have initialized the test data and invoked the GET /api/customers API endpoint and verified that 2 customer records are returned from the API.

Run tests

./mvnw test

You should see the Postgres docker container is started and all tests should PASS. You can also notice that after tests are executed the containers are stopped and removed automatically.