- Java 17+
- Any IDE (Intellij IDEA, Eclipse/STS, NetBeans, VS Code)
- A Docker environment supported by Testcontainers (Supported Docker Environment)
We are going to create a Spring Boot project using Spring Data JPA with Postgres and implement a REST API endpoint to return all the policy details that are stored in the database. Then we will test this API using the Testcontainers Postgres module and RestAssured.
Let's create a new Spring Boot project from Spring Initializr by selecting the Spring Web, Spring Data JPA, PostgreSQL Driver, and Testcontainers starters.
For the sake of simplicity, in the application, we'll be having one Controller, one Entity, and one Repository class. The functionality will be fetching all the Policies using JPA and testing them through TestContainers. Below will be the project structure:
Implementation:
STEP:1 | Required dependencies
Below are the MAVEN dependencies required to proceed:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</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>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
Here is the full 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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.testcontainer</groupId>
<artifactId>TestContainer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>TestContainer</name>
<description>Demo project for Test Container</description>
<properties>
<java.version>17</java.version>
<testcontainers.version>1.18.0</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.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</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>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</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>
</plugins>
</build>
</project>
Alternatively, below are the GRADLE dependencies required:
ext {
set('testcontainersVersion', "1.18.0")
}
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'
}
STEP:2 | Create JPA entity:
Let's create the JPA Entity class which is basically the representation of our policy database object (Policy.java).
package com.testcontainer.demo.entity;
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 = "policies")
public class Policy {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String polnumber;
@Column(nullable = false)
private String polstatus;
public Policy() {}
public Policy(Long id, String polnumber, String polstatus) {
this.id = id;
this.polnumber = polnumber;
this.polstatus = polstatus;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getPolNumber() {
return polnumber;
}
public void setPolNumber(String polnumber) {
this.polnumber = polnumber;
}
public String getPolStatus() {
return polstatus;
}
public void setPolStatus(String polstatus) {
this.polstatus = polstatus;
}
}
STEP:3 | Create Spring Data JPA repository:
PolicyRepository.java
package com.testcontainer.demo.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.testcontainer.demo.entity.Policy;
public interface PolicyRepository extends JpaRepository<Policy, Long> {}
STEP:4 | Add schema creation script:
As we are not using any in-memory database, we need to create the Postgres database tables. Again for the sake of simplicity, in this example, we will use simple schema initialization support provided by Spring Boot (also we can use some database migration tools like Flyway or Liquibase).
Create a schema.sql file with the following content under the src/main/resources directory.
create table if not exists policies (
id bigserial not null,
polnumber varchar not null,
polstatus varchar not null,
primary key (id),
UNIQUE (polnumber)
);
STEP:5 | Property/Configuration changes:
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
STEP:6 | Create REST API endpoint:
Finally, create a controller to implement a REST API endpoint to fetch all policies from the database.
PolicyController.java
package com.testcontainer.demo.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.testcontainer.demo.entity.Policy;
import com.testcontainer.demo.repository.PolicyRepository;
@RestController
@RequestMapping("/test-containers/apis")
public class PolicyController {
@Autowired
private final PolicyRepository repo;
public PolicyController(PolicyRepository repo) {
this.repo = repo;
}
@GetMapping("/policies")
public List<Policy> getAll() {
return repo.findAll();
}
}
STEP:7 | Write the test for API endpoint:
We are going to write a test for the REST API GET /test-containers/apis/policies endpoint by starting the Spring context using the @SpringBootTest annotation and invoking the APIs using RestAssured.
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:
PolicyControllerTest.java
package com.testcontainer.demo.controller;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.hasSize;
import java.util.List;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
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;
import com.testcontainer.demo.entity.Policy;
import com.testcontainer.demo.repository.PolicyRepository;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class PolicyControllerTest {
@LocalServerPort
private Integer port;
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
"postgres:15-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
PolicyRepository policyRepository;
@BeforeEach
void setUp() {
RestAssured.baseURI = "http://localhost:" + port;
policyRepository.deleteAll();
}
@Test
public void givenUrl_whenJsonResponseHasGivenValuesUnderKey_thenCorrect() {
List<Policy> customers = List.of(
new Policy(null, "POL-12345", "Active"),
new Policy(null, "POL-12346", "Active")
);
policyRepository.saveAll(customers);
given()
.contentType(ContentType.JSON)
.when()
.get("/test-containers/apis/policies")
.then()
.statusCode(200)
.body(".", hasSize(2)).log().all();
}
}
Test Explanation:
Let's understand:
- 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:15-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 randomly available port on the host. We have registered the database connection properties dynamically obtained from the Postgres container using Spring Boot’s DynamicPropertyRegistry (please refer to the below highlighted screenshot).
- We have injected the random port on which the Spring Boot application started using @LocalServerPort and registered the RestAssured baseURI (please refer to the below highlighted screenshot).
- We are deleting all customer rows using JUnit 5 @BeforeEach callback method which gets executed before every test method. This will ensure a predictable data setup for every test and circumvent any kind of test pollution (please refer to the below highlighted screenshot).
- Finally, in the givenUrl_whenJsonResponseHasGivenValuesUnderKey_thenCorrect() test, we have initialized the test data and invoked the GET /test-containers/apis/policies API endpoint and verified that 2 policy records are returned from the API (please refer to the below highlighted screenshot).
STEP:8 | Run the test:
Now whenever we'll be running the test, it'll use temporary docker images to execute the tests as below. Make sure the docker is running, and run the test.
Connected to local docker and successfully created all the required containers.
The test is successfully completed.
During the test, below the containers are created and once the test is done, it'll be destroyed by itself as we mentioned in the test case (@AfterAll annotation)
Conclusion
We've just witnessed the most straightforward and easiest method to set up PostgreSQL for Spring Boot testing using Testcontainers. With the help of Testcontainers, we can test our application against the infrastructure that is used at runtime, without any setup costs, it is quite easy to use for the most common use cases. The configuration would be almost the same for other databases. 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 real services, we are free to do any code refactoring and still ensure that the application is working as expected.
In summary, Spring Boot and Testcontainers make for an exceptionally adaptable pairing.
Resources