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:
- 1. Project Structure
- 2. Project Dependencies
- 3. Project Dependencies (Tree Format)
- 4. Spring Data JPA – Entity and Repository
- 5. SQL create table
- 6. Spring Web REST endpoints
- 7. Write the tests for Spring Boot Testcontainers
- 8. Spring Data JPA @DataJpaTest and Testcontainers
- 9. Run the Tests
- 10. FAQs
- 11. Download Source Code
- 12. References
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.
2. Project Dependencies
Puts the Spring Boot starters and Testcontainers dependencies.
<?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
.
[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.
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
.
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
.
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.
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.
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.
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.
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
, andstatic 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 theTestcontainers
on runtime. - The image
postgres:15-alpine
means PostgreSQL 15, Alpine Linux base image. TheTestcontainers
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.
@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.
<!-- Testcontainers JUnit 5 Extension -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
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.
<!-- Spring Boot 3.1 and @ServiceConnection -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
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
.
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.
./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
@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.
@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
- Spring Boot and Testcontainers
- Spring Boot Doc Database Initialization
- Spring Test using Testcontainers
- PostgreSQL Versioning Policy
- Testcontainers Postgres Module
- Spring Boot + JUnit 5 + Mockito
- Spring REST Hello World Example
- Spring Boot JDBC Examples
- Spring data JPA and MySQL example
- Spring Data JPA and PostgreSQL example
- Testing Spring Data JPA with @DataJpaTest
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