Getting started with Testcontainers in a Java Spring Boot Project

  • Java
  • Spring Boot
  • PostgreSQL
  • REST Assured
Get the code

In this guide, you will learn how to

  • Create a Spring Boot application

  • Implement a REST API endpoint using Spring MVC, Spring Data JPA, and Postgres

  • Test REST API using Testcontainers and RestAssured.

Prerequisites

What we are going to achieve in this guide

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 Web, Spring Data JPA, PostgreSQL Driver and Testcontainers starters.

Instead, you can clone https://github.com/testcontainers/tc-guide-testing-spring-boot-rest-api.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>

If you have selected the Gradle build tool instead, then the build.gradle file is configured with the selected dependencies.

ext {
    set('testcontainers.version', "1.19.8")
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    runtimeOnly 'org.postgresql:postgresql'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.testcontainers:junit-jupiter'
    testImplementation 'org.testcontainers:postgresql'
}

Using the Testcontainers' BOM (Bill Of Material) is highly recommended so that you don’t have to repeat the Testcontainers version for every individual Testcontainers module dependency

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<Customer, Long> {}

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<Customer> 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>

If you are using Gradle build tool then add the following dependency in build.gradle file.

testImplementation 'io.rest-assured:rest-assured'

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<Customer> 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

# If you are using Maven
./mvnw test

# If you are using Gradle
./gradlew 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.

Summary

The Testcontainers library helped us to write integration tests by using the same type of database, Postgres, that we use in production as opposed to Mocks or in-memory databases. As we are not using mocks and talking to the real services, we are free to do any code refactoring and still ensure that the application is working as expected.

To learn more about Testcontainers visit http://testcontainers.com