Introduction
The @DataJpaTest annotation in Spring Boot is commonly used for testing JPA repositories in isolation. By default, it sets up an in-memory H2 database, but what if we want to test with a real database such as PostgreSQL without installing it locally?
This is where Testcontainers comes in.

What is Testcontainers?
Testcontainers is a Java library that allows running lightweight, disposable containers for databases, message queues, and other dependencies. It is particularly useful for integration testing as it provides:
✅ Real database testing without requiring local installation.
✅ Fresh database instances for each test execution.
✅ Automatic cleanup of containers after tests.
✅ Consistent testing environments across different machines.
Project Setup
To get started, create a Spring Boot project with the following dependencies:
- Spring Data JPA – For database interaction.
- PostgreSQL Driver – To connect to a PostgreSQL database.
- Spring Boot Starter Test – Includes JUnit, AssertJ, and Mockito.
- Testcontainers – To run a PostgreSQL database in a Docker container.

Spring Boot Testing Libraries
The spring-boot-starter-test dependency provides several useful testing tools:
- JUnit – The core testing framework.
- Spring Test & Spring Boot Test – Utilities for integration and context-based testing.
- AssertJ – Fluent assertion library.
- Mockito – A mocking framework for unit tests.
- Testcontainers – Allows running a real PostgreSQL database in a container.
Implementation

1. pom.xml Configuration
To use Testcontainers with PostgreSQL, add the necessary dependencies to pom.xml.
File: 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.4.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>tech.devblueprint</groupId>
<artifactId>datajpatest-testcontainers-spring-boot-testing-example</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>datajpatest-testcontainers-spring-boot-testing-example</name>
<description>Demo project for Spring Boot</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</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>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2. Database Configuration
By default, Spring Boot expects a database connection in application.yaml. However, since we are using Testcontainers, we don’t need a static database URL. Instead, we dynamically provide connection properties in the test class.
3. Entity Definition
File: src/main/java/.../entity/Device.java
package tech.devblueprint.datajpatest_spring_boot_testing_example.entity;
import jakarta.persistence.*;
@Entity
@Table(name = "devices")
public class Device {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String type;
private Double price;
public Device() {
}
public Device(String name, String type, Double price) {
this.name = name;
this.type = type;
this.price = price;
}
// Getters и Setters
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 getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
}
4. Repository Interface
File: src/main/java/.../repository/DeviceRepository.java
package tech.devblueprint.datajpatest_spring_boot_testing_example.repository;
import tech.devblueprint.datajpatest_spring_boot_testing_example.entity.Device;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
import java.util.Optional;
public interface DeviceRepository extends JpaRepository<Device, Long> {
// Derived Query
List<Device> findByType(String type);
// JPQL-Query
@Query("FROM Device d WHERE d.price > ?1")
List<Device> findDevicesByPriceGreaterThan(Double price);
// Native SQL-Query
@Query(value = "SELECT * FROM devices WHERE name = :name", nativeQuery = true)
Optional<Device> findByName(String name);
}
Writing Tests with Testcontainers
Using a PostgreSQL Container in Tests
With Testcontainers, we can dynamically start a PostgreSQL container and register its connection details for Spring Boot.
File: src/test/java/.../repository/TestcontainersDeviceRepositoryTests.java
package tech.devblueprint.datajpatest_testcontainers_spring_boot_testing_example.repository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.jdbc.Sql;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import tech.devblueprint.datajpatest_testcontainers_spring_boot_testing_example.entity.Device;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
@Testcontainers
@DataJpaTest
@Sql({"/test-device-schema.sql"}) // Execute the schema creation script
public class TestcontainersDeviceRepositoryTests {
// Start PostgreSQL container using Testcontainers
@Container
public static PostgreSQLContainer<?> postgresContainer = new PostgreSQLContainer<>("postgres:16.3")
.withDatabaseName("testdb")
.withUsername("postgres")
.withPassword("root");
// Dynamically register properties so that Spring Boot uses the container's JDBC URL and credentials
@DynamicPropertySource
static void registerProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgresContainer::getJdbcUrl);
registry.add("spring.datasource.username", postgresContainer::getUsername);
registry.add("spring.datasource.password", postgresContainer::getPassword);
}
@Autowired
private DeviceRepository deviceRepository;
// Test using Testcontainers to verify that the repository works with the PostgreSQL container
@Test
@Sql({"/test-device-data.sql"}) // Execute the test data insertion script
public void testFindByName_UsingTestcontainers() {
Optional<Device> deviceOpt = deviceRepository.findByName("iPhone 13");
assertThat(deviceOpt).isPresent();
Device device = deviceOpt.get();
assertThat(device.getName()).isEqualTo("iPhone 13");
assertThat(device.getType()).isEqualTo("Smartphone");
assertThat(device.getPrice()).isEqualTo(999.99);
}
}
Key Annotations Explained:
@Testcontainers– Enables Testcontainers integration.@Container– Defines a PostgreSQL container that runs for the duration of the test class.@DynamicPropertySource– Registers the container’s JDBC URL and credentials dynamically.@DataJpaTest– Loads only JPA-related components for testing.@Sql("/test-device-schema.sql")– Runs the schema creation script before tests.@Sql("/test-device-data.sql")– Loads test data before each test.
Understanding Transactions in @DataJpaTest
By default, tests annotated with @DataJpaTest run in transactions and automatically roll back at the end of each test. This ensures that each test starts with a clean database state.
However, if you need to disable transaction management, use @Transactional(propagation = Propagation.NOT_SUPPORTED), as shown below:
@DataJpaTest
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public class StudentRepositoryTests {
@Autowired
private StudentRepository studentRepository;
@Test
void findByName_ReturnsTheStudent() {
// Test logic...
}
}
This is useful when testing database operations that require commits, such as stored procedures or triggers.
Example SQL Scripts
SQL scripts should be placed in src/test/resources so that Spring Boot can automatically load them during test execution.
Schema Definition
File: src/test/resources/test-device-schema.sql
CREATE TABLE devices
(
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
type VARCHAR(100),
price DOUBLE
);
Test Data Insertion
File: src/test/resources/test-device-data.sql
INSERT INTO devices (id, name, type, price)
VALUES (1, 'iPhone 13', 'Smartphone', 999.99),
(2, 'Galaxy S22', 'Smartphone', 899.99),
(3, 'Dell XPS 13', 'Laptop', 1199.99),
(4, 'MacBook Pro', 'Laptop', 1999.99);
Conclusion
In this guide, we explored how to use Testcontainers with @DataJpaTest to test JPA repositories with a real PostgreSQL database.