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.