Main Tutorials

Spring Boot Testcontainers Example

This article shows how to test the Spring Boot REST endpoints using TestRestTemplate and Testcontainers (PostgreSQL container).

Technologies used :

  • Spring Boot 3.1.2 (Spring Web MVC, Spring Data JPA and Spring Test)
  • Testcontainers 1.19.0
  • PostgreSQL 15, Alpine Linux base image postgres:15-alpine
  • Java 17
  • JUnt 5

Tables of contents:

Note
Testcontainers is an open-source Java library that manages Docker containers in tests, enabling lightweight, disposable instances of databases, message brokers, and other services for integration testing.

P.S. The computer running the Testcontainers’s tests needs to install Docker.

1. Project Structure

Below is the standard Java project structure for this article.

Spring Boot Testcontainers project structure

2. Project Dependencies

Puts the Spring Boot starters and Testcontainers dependencies.

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
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <artifactId>spring-boot-testcontainers</artifactId>
    <packaging>jar</packaging>
    <url>https://mkyong.com</url>
    <version>1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.1.2</version>
        <relativePath/> <!-- lookup parent from repository, not local -->
    </parent>

    <properties>
        <java.version>17</java.version>
        <testcontainers.version>1.19.0</testcontainers.version>
    </properties>

    <dependencies>

        <!-- REST endpoints -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Spring Data JPA -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <!-- Spring Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- Spring Boot 3.1 and @ServiceConnection -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-testcontainers</artifactId>
            <scope>test</scope>
        </dependency>

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

        <!-- TestContainers -->
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>testcontainers</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>postgresql</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- Testcontainers JUnit 5 Extension -->
        <!-- @Testcontainers and @Container to star and stop container -->
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>junit-jupiter</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.testcontainers</groupId>
                <artifactId>testcontainers-bom</artifactId>
                <version>${testcontainers.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.11.0</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

3. Project Dependencies (Tree Format)

The Testcontainers has some dependencies on docker-java.

Terminal

[INFO] +- org.testcontainers:testcontainers:jar:1.19.0:test
[INFO] |  +- junit:junit:jar:4.13.2:test
[INFO] |  |  \- org.hamcrest:hamcrest-core:jar:2.2:test
[INFO] |  +- org.slf4j:slf4j-api:jar:2.0.7:compile
[INFO] |  +- org.apache.commons:commons-compress:jar:1.23.0:test
[INFO] |  +- org.rnorth.duct-tape:duct-tape:jar:1.0.8:test
[INFO] |  |  \- org.jetbrains:annotations:jar:17.0.0:test
[INFO] |  +- com.github.docker-java:docker-java-api:jar:3.3.3:test
[INFO] |  |  \- com.fasterxml.jackson.core:jackson-annotations:jar:2.15.2:compile
[INFO] |  \- com.github.docker-java:docker-java-transport-zerodep:jar:3.3.3:test
[INFO] |     +- com.github.docker-java:docker-java-transport:jar:3.3.3:test
[INFO] |     \- net.java.dev.jna:jna:jar:5.12.1:test
[INFO] \- org.testcontainers:postgresql:jar:1.19.0:test
[INFO]    \- org.testcontainers:jdbc:jar:1.19.0:test
[INFO]       \- org.testcontainers:database-commons:jar:1.19.0:test

Review the entire project dependencies in tree format.

pom.xml

mvn dependency:tree

mvn dependency:tree
[INFO] Scanning for projects...
[INFO]
[INFO] --------< org.springframework.boot:spring-boot-testcontainers >---------
[INFO] Building spring-boot-testcontainers 1.0
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-dependency-plugin:3.5.0:tree (default-cli) @ spring-boot-testcontainers ---
[INFO] org.springframework.boot:spring-boot-testcontainers:jar:1.0
[INFO] +- org.springframework.boot:spring-boot-starter-web:jar:3.1.2:compile
[INFO] |  +- org.springframework.boot:spring-boot-starter:jar:3.1.2:compile
[INFO] |  |  +- org.springframework.boot:spring-boot:jar:3.1.2:compile
[INFO] |  |  +- org.springframework.boot:spring-boot-starter-logging:jar:3.1.2:compile
[INFO] |  |  |  +- ch.qos.logback:logback-classic:jar:1.4.8:compile
[INFO] |  |  |  |  \- ch.qos.logback:logback-core:jar:1.4.8:compile
[INFO] |  |  |  +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.20.0:compile
[INFO] |  |  |  |  \- org.apache.logging.log4j:log4j-api:jar:2.20.0:compile
[INFO] |  |  |  \- org.slf4j:jul-to-slf4j:jar:2.0.7:compile
[INFO] |  |  +- jakarta.annotation:jakarta.annotation-api:jar:2.1.1:compile
[INFO] |  |  \- org.yaml:snakeyaml:jar:1.33:compile
[INFO] |  +- org.springframework.boot:spring-boot-starter-json:jar:3.1.2:compile
[INFO] |  |  +- com.fasterxml.jackson.core:jackson-databind:jar:2.15.2:compile
[INFO] |  |  |  \- com.fasterxml.jackson.core:jackson-core:jar:2.15.2:compile
[INFO] |  |  +- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:jar:2.15.2:compile
[INFO] |  |  +- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:jar:2.15.2:compile
[INFO] |  |  \- com.fasterxml.jackson.module:jackson-module-parameter-names:jar:2.15.2:compile
[INFO] |  +- org.springframework.boot:spring-boot-starter-tomcat:jar:3.1.2:compile
[INFO] |  |  +- org.apache.tomcat.embed:tomcat-embed-core:jar:10.1.11:compile
[INFO] |  |  +- org.apache.tomcat.embed:tomcat-embed-el:jar:10.1.11:compile
[INFO] |  |  \- org.apache.tomcat.embed:tomcat-embed-websocket:jar:10.1.11:compile
[INFO] |  +- org.springframework:spring-web:jar:6.0.11:compile
[INFO] |  |  +- org.springframework:spring-beans:jar:6.0.11:compile
[INFO] |  |  \- io.micrometer:micrometer-observation:jar:1.11.2:compile
[INFO] |  |     \- io.micrometer:micrometer-commons:jar:1.11.2:compile
[INFO] |  \- org.springframework:spring-webmvc:jar:6.0.11:compile
[INFO] |     +- org.springframework:spring-aop:jar:6.0.11:compile
[INFO] |     +- org.springframework:spring-context:jar:6.0.11:compile
[INFO] |     \- org.springframework:spring-expression:jar:6.0.11:compile
[INFO] +- org.springframework.boot:spring-boot-starter-data-jpa:jar:3.1.2:compile
[INFO] |  +- org.springframework.boot:spring-boot-starter-aop:jar:3.1.2:compile
[INFO] |  |  \- org.aspectj:aspectjweaver:jar:1.9.19:compile
[INFO] |  +- org.springframework.boot:spring-boot-starter-jdbc:jar:3.1.2:compile
[INFO] |  |  +- com.zaxxer:HikariCP:jar:5.0.1:compile
[INFO] |  |  \- org.springframework:spring-jdbc:jar:6.0.11:compile
[INFO] |  +- org.hibernate.orm:hibernate-core:jar:6.2.6.Final:compile
[INFO] |  |  +- jakarta.persistence:jakarta.persistence-api:jar:3.1.0:compile
[INFO] |  |  +- jakarta.transaction:jakarta.transaction-api:jar:2.0.1:compile
[INFO] |  |  +- org.jboss.logging:jboss-logging:jar:3.5.3.Final:runtime
[INFO] |  |  +- org.hibernate.common:hibernate-commons-annotations:jar:6.0.6.Final:runtime
[INFO] |  |  +- io.smallrye:jandex:jar:3.0.5:runtime
[INFO] |  |  +- com.fasterxml:classmate:jar:1.5.1:runtime
[INFO] |  |  +- net.bytebuddy:byte-buddy:jar:1.14.5:runtime
[INFO] |  |  +- org.glassfish.jaxb:jaxb-runtime:jar:4.0.3:runtime
[INFO] |  |  |  \- org.glassfish.jaxb:jaxb-core:jar:4.0.3:runtime
[INFO] |  |  |     +- org.eclipse.angus:angus-activation:jar:2.0.1:runtime
[INFO] |  |  |     +- org.glassfish.jaxb:txw2:jar:4.0.3:runtime
[INFO] |  |  |     \- com.sun.istack:istack-commons-runtime:jar:4.1.2:runtime
[INFO] |  |  +- jakarta.inject:jakarta.inject-api:jar:2.0.1:runtime
[INFO] |  |  \- org.antlr:antlr4-runtime:jar:4.10.1:compile
[INFO] |  +- org.springframework.data:spring-data-jpa:jar:3.1.2:compile
[INFO] |  |  +- org.springframework.data:spring-data-commons:jar:3.1.2:compile
[INFO] |  |  +- org.springframework:spring-orm:jar:6.0.11:compile
[INFO] |  |  \- org.springframework:spring-tx:jar:6.0.11:compile
[INFO] |  \- org.springframework:spring-aspects:jar:6.0.11:compile
[INFO] +- org.springframework.boot:spring-boot-starter-test:jar:3.1.2:test
[INFO] |  +- org.springframework.boot:spring-boot-test:jar:3.1.2:test
[INFO] |  +- org.springframework.boot:spring-boot-test-autoconfigure:jar:3.1.2:test
[INFO] |  +- com.jayway.jsonpath:json-path:jar:2.8.0:test
[INFO] |  +- jakarta.xml.bind:jakarta.xml.bind-api:jar:4.0.0:runtime
[INFO] |  |  \- jakarta.activation:jakarta.activation-api:jar:2.1.2:runtime
[INFO] |  +- net.minidev:json-smart:jar:2.4.11:test
[INFO] |  |  \- net.minidev:accessors-smart:jar:2.4.11:test
[INFO] |  |     \- org.ow2.asm:asm:jar:9.3:test
[INFO] |  +- org.assertj:assertj-core:jar:3.24.2:test
[INFO] |  +- org.hamcrest:hamcrest:jar:2.2:test
[INFO] |  +- org.junit.jupiter:junit-jupiter:jar:5.9.3:test
[INFO] |  |  +- org.junit.jupiter:junit-jupiter-api:jar:5.9.3:test
[INFO] |  |  |  +- org.opentest4j:opentest4j:jar:1.2.0:test
[INFO] |  |  |  +- org.junit.platform:junit-platform-commons:jar:1.9.3:test
[INFO] |  |  |  \- org.apiguardian:apiguardian-api:jar:1.1.2:test
[INFO] |  |  +- org.junit.jupiter:junit-jupiter-params:jar:5.9.3:test
[INFO] |  |  \- org.junit.jupiter:junit-jupiter-engine:jar:5.9.3:test
[INFO] |  |     \- org.junit.platform:junit-platform-engine:jar:1.9.3:test
[INFO] |  +- org.mockito:mockito-core:jar:5.3.1:test
[INFO] |  |  +- net.bytebuddy:byte-buddy-agent:jar:1.14.5:test
[INFO] |  |  \- org.objenesis:objenesis:jar:3.3:test
[INFO] |  +- org.mockito:mockito-junit-jupiter:jar:5.3.1:test
[INFO] |  +- org.skyscreamer:jsonassert:jar:1.5.1:test
[INFO] |  |  \- com.vaadin.external.google:android-json:jar:0.0.20131108.vaadin1:test
[INFO] |  +- org.springframework:spring-core:jar:6.0.11:compile
[INFO] |  |  \- org.springframework:spring-jcl:jar:6.0.11:compile
[INFO] |  +- org.springframework:spring-test:jar:6.0.11:test
[INFO] |  \- org.xmlunit:xmlunit-core:jar:2.9.1:test
[INFO] +- org.springframework.boot:spring-boot-testcontainers:jar:3.1.2:test
[INFO] |  \- org.springframework.boot:spring-boot-autoconfigure:jar:3.1.2:compile
[INFO] +- org.postgresql:postgresql:jar:42.6.0:runtime
[INFO] |  \- org.checkerframework:checker-qual:jar:3.31.0:runtime
[INFO] +- org.testcontainers:testcontainers:jar:1.19.0:test
[INFO] |  +- junit:junit:jar:4.13.2:test
[INFO] |  |  \- org.hamcrest:hamcrest-core:jar:2.2:test
[INFO] |  +- org.slf4j:slf4j-api:jar:2.0.7:compile
[INFO] |  +- org.apache.commons:commons-compress:jar:1.23.0:test
[INFO] |  +- org.rnorth.duct-tape:duct-tape:jar:1.0.8:test
[INFO] |  |  \- org.jetbrains:annotations:jar:17.0.0:test
[INFO] |  +- com.github.docker-java:docker-java-api:jar:3.3.3:test
[INFO] |  |  \- com.fasterxml.jackson.core:jackson-annotations:jar:2.15.2:compile
[INFO] |  \- com.github.docker-java:docker-java-transport-zerodep:jar:3.3.3:test
[INFO] |     +- com.github.docker-java:docker-java-transport:jar:3.3.3:test
[INFO] |     \- net.java.dev.jna:jna:jar:5.12.1:test
[INFO] +- org.testcontainers:postgresql:jar:1.19.0:test
[INFO] |  \- org.testcontainers:jdbc:jar:1.19.0:test
[INFO] |     \- org.testcontainers:database-commons:jar:1.19.0:test
[INFO] \- org.testcontainers:junit-jupiter:jar:1.19.0:test
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  1.356 s
[INFO] Finished at: 2023-09-19T15:36:45+08:00
[INFO] ------------------------------------------------------------------------

4. Spring Data JPA – Entity and Repository

4.1 Create a JPA entity Book.java.

Book.java

package com.mkyong.book;

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

@Entity
@Table(name = "books")
public class Book {

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

    @Column(nullable = false)
    private String name;

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

    public Book() {
    }

    //... getetrs, setters, constructor and etc
}

4.2 Creates an interface and extends the JpaRepository<Book, Long>, and we have basic CRUD operations for the entity Book.

BookRepository.java

package com.mkyong.book;

import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;

public interface BookRepository extends JpaRepository<Book, Long> {

    Optional<Book> findByIsbn(String isbn);

}

5. SQL create table

5.1 We can set the property spring.sql.init.mode to always, which makes Spring will always run the database initialization scripts (schema.sql and data.sql) on startup.

application.properties

  spring.sql.init.mode=always

Database initialization scripts:

  • schema.sql: Contains SQL DDL statements to create the schema.
  • data.sql: Contains SQL DML statements to populate the schema with initial data.

5.2 Creates a schema.sql script and put it under the src/main/resources/ folder.

src/main/resources/schema.sql

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

In the above example, Spring always runs the schema.sql script on startup and creates the table books for tests.

6. Spring Web REST endpoints

Below are Spring REST endpoints for the book entity.

BookController.java

package com.mkyong.book;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/books")
public class BookController {

  @Autowired
  private BookRepository bookRepository;

  @GetMapping
  public List<Book> getAll() {
      return bookRepository.findAll();
  }

  @PostMapping
  public Book create(@RequestBody Book book) {
      return bookRepository.save(book);
  }

  @GetMapping("/{id}")
  public Book getById(@PathVariable Long id) {
      return bookRepository.findById(id).orElse(null);
  }
}

7. Write the tests for Spring Boot Testcontainers

Below are Spring Boot integration tests using Testcontainers; We can use TestRestTemplate to test the Spring REST endpoints.

BookControllerOldWayTest.java

package com.mkyong.book;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
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.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;

import static org.junit.jupiter.api.Assertions.assertEquals;

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

    @LocalServerPort
    private Integer port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private BookRepository bookRepository;

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

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

    /**
     * postgres:15-alpine
     * PostgreSQL version 15 using the lightweight Alpine Linux as the base image
     */
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
            "postgres:15-alpine"
    );

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

    @Test
    public void testBookEndpoints() {

        // Create a new book
        Book book = new Book();
        book.setName("Is Java Dead?");
        book.setIsbn("111-111");

        ResponseEntity<Book> createResponse =
                restTemplate.postForEntity("/books", book, Book.class);
        assertEquals(HttpStatus.OK, createResponse.getStatusCode());
        Book savedBook = createResponse.getBody();

        assert savedBook != null;

        // Retrieve
        ResponseEntity<Book> getResponse =
                restTemplate.getForEntity("/books/" + savedBook.getId(), Book.class);
        assertEquals(HttpStatus.OK, getResponse.getStatusCode());

        Book bookFromGet = getResponse.getBody();

        assert bookFromGet != null;

        assertEquals("Is Java Dead?", bookFromGet.getName());
        assertEquals("111-111", bookFromGet.getIsbn());

        // Retrieve All
        ResponseEntity<Book[]> getAllResponse =
                restTemplate.getForEntity("/books", Book[].class);
        assertEquals(HttpStatus.OK, getAllResponse.getStatusCode());

        Book[] bookFromGetAll = getAllResponse.getBody();
        assert bookFromGetAll != null;

        assertEquals(1, bookFromGetAll.length);
    }

}

About the tests:

  • The @SpringBootTest starts the entire Spring Boot web environment for tests.
  • The JUnit @BeforeAll, @AfterAll, and static PostgreSQLContainer ensures that the container is started before any test methods and is stopped after all test methods are completed.
  • We use the Spring Test @DynamicPropertySource to dynamically register the database properties from the Testcontainers on runtime.
  • The image postgres:15-alpine means PostgreSQL 15, Alpine Linux base image. The Testcontainers will use this PostgreSQL container for the tests.
  • We use the Spring Test TestRestTemplate to request HTTP to the BookController’s endpoints and assert the expected responses.

7.1 Testcontainers JUnit 5 Extension

In the above Testcontainers example, we use the JUnit 5 lifecycle to start and stop the container.

BookControllerOldWayTest.java

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

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

With the Testcontainers JUnit 5 Extension, we can use @Testcontainers and @Container to automatically start and stop the container.

pom.xml

  <!-- Testcontainers JUnit 5 Extension -->
  <dependency>
      <groupId>org.testcontainers</groupId>
      <artifactId>junit-jupiter</artifactId>
      <scope>test</scope>
  </dependency>
BookControllerTest.java

import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
//new
@Testcontainers
public class BookControllerTest {

  // no need this, the @Testcontainers and @Container will auto start and stop the container.
  /*@BeforeAll
  static void beforeAll() {
      postgres.start();
  }

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

  // new
  @Container
  static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
          "postgres:15-alpine"
  );

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

  //...
}

7.2 Spring Boot 3.1.0 @ServiceConnection

In Spring Boot 3.1.0, we can use the @ServiceConnection annotation to register the Database connection into the @Container, insteads of using the @DynamicPropertySource annotation.

pom.xml

  <!-- Spring Boot 3.1 and @ServiceConnection -->
  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-testcontainers</artifactId>
      <scope>test</scope>
  </dependency>
BookControllerTest.java

import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;

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

  @Container
  @ServiceConnection
  static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
          "postgres:15-alpine"
  );


  // With Spring Boot 3.1 and @ServiceConnection, no need this @DynamicPropertySource
  /*
  @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);
  }*/

  //...
}

Further Reading
Read the official Spring Boot guide on Testcontainers.

8. Spring Data JPA @DataJpaTest and Testcontainers

This example shows how to test the repository using only the @DataJpaTest and Testcontainers.

BookRepositoryTest.java

package com.mkyong.book;

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.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

@DataJpaTest
// do not replace the testcontainer data source
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
public class BookRepositoryTest {

    @Autowired
    private BookRepository bookRepository;

    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
            "postgres:15-alpine"
    );

    @Test
    public void testBookSaveAndFindById() {

        // Create a new book
        Book book = new Book();
        book.setName("Is Java Dead?");
        book.setIsbn("111-111");

        // save book
        bookRepository.save(book);

        // find book
        Optional<Book> result = bookRepository.findById(book.getId());
        assertTrue(result.isPresent());

        Book bookFromGet = result.get();

        assertEquals("Is Java Dead?", bookFromGet.getName());
        assertEquals("111-111", bookFromGet.getIsbn());

    }

    @Test
    public void testBookCRUD() {

        Book book = new Book();
        book.setName("Is Java Dead?");
        book.setIsbn("111-111");

        // save book
        bookRepository.save(book);

        // find book by isbn
        Optional<Book> result = bookRepository.findByIsbn(book.getIsbn());
        assertTrue(result.isPresent());

        Book bookFromGet = result.get();

        Long bookId = bookFromGet.getId();

        assertEquals("Is Java Dead?", bookFromGet.getName());
        assertEquals("111-111", bookFromGet.getIsbn());

        // update book
        book.setName("Java still relevant in 2050");
        bookRepository.save(book);

        // find book by id
        Optional<Book> result2 = bookRepository.findById(bookId);
        assertTrue(result2.isPresent());

        Book bookFromGet2 = result2.get();

        assertEquals("Java still relevant in 2050", bookFromGet2.getName());
        assertEquals("111-111", bookFromGet2.getIsbn());

        // delete a book
        bookRepository.delete(book);

        // should be empty
        assertTrue(bookRepository.findById(bookId).isEmpty());

    }

}

9. Run the Tests

Run the Spring Boot Testcontainers tests.

Terminal

  ./mvnw test

All the tests should passed.

10. FAQs

10.1 Question: java.lang.IllegalStateException: Could not find a valid Docker environment
Answer: Please ensure the Docker is installed properly on the computer running the tests.

10.2 Question: In @SpringBootTest, failed to @Autowired TestRestTemplate

BookRepositoryTest.java

@SpringBootTest
public class BookRepositoryTest {

    @Autowired
    private TestRestTemplate restTemplate;

Answer: Please ensure the @SpringBootTest includes the webEnvironment argument to start the web server for the tests.

BookRepositoryTest.java

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

    @Autowired
    private TestRestTemplate restTemplate;

10.3 Question: Why declare static for the container?
Answer:

  • If the container is a static field, it will be initiated once before all the tests and terminated after all the tests. Which means all tests share a single container.
  • If the container is a non-static field, it will be initiated before each test and terminated after each test. Which means each test has its container.

@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
        "postgres:15-alpine"
);

11. Download Source Code

$ git clone https://github.com/mkyong/spring-boot.git

$ cd spring-boot-testcontainers

$ ./mvnw test

$ ./mvnw spring-boot:run

12. References

About Author

author image
Founder of Mkyong.com, love Java and open source stuff. Follow him on Twitter. If you like my tutorials, consider make a donation to these charities.

Comments

Subscribe
Notify of
1 Comment
Most Voted
Newest Oldest
Inline Feedbacks
View all comments
Hamide
1 month ago

Hi mkyong thanks for this interesting topic. I implemented for repository Test with @DataJpaTest and it work but only for reading data. When I tried to save data it thows an exception. See below
Here my test class confi
@DataJpaTest
@Testcontainers
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class WalletProviderManagementRepositoryUnitTest {

@Container
@ServiceConnection
private static PostgreSQLContainer postgres = new PostgreSQLContainer(“postgres”);

@Autowired
private WalletProviderRepository walletProviderRepository;

here the test
@Test
public void shouldSaveWalletProvider(){
var toBeSaved = WalletProvider
.builder()
.providerName(“Name”)
.providerCode(“Code”)
.build();

var saved = walletProviderRepository.save(toBeSaved);
assertThat(saved.getId() == 2).isTrue();
}

And here the exception
Caused by: org.postgresql.util.PSQLException: ERROR: relation “wallet_provider_seq” does not exist

I remind that I put schema.sql and application.properties.

Do you have any idea?
Thanks