I want to use Testcontainers with @DataJpaTest
(and @SpringBootTest
) using JUnit 5. I have the basic setup working using the @Testcontainers
and @Container
annotation like this:
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.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; import org.testcontainers.containers.PostgreSQLContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import static org.assertj.core.api.Assertions.assertThat; @DataJpaTest @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) @Testcontainers public class AtleteRepositoryTest { @Container private static final PostgreSQLContainer<?> CONTAINER = new PostgreSQLContainer<>("postgres:11"); @DynamicPropertySource static void registerProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", CONTAINER::getJdbcUrl); registry.add("spring.datasource.username", CONTAINER::getUsername); registry.add("spring.datasource.password", CONTAINER::getPassword); } @Autowired private AtleteRepository repository; @Test void testSave() { repository.save(new Atlete("Wout Van Aert", 0, 1, 0)); assertThat(repository.count()).isEqualTo(1); } }
See https://github.com/wimdeblauwe/blog-example-code/tree/feature/testcontainers-datajpatest/testcontainers-datajpatest for the full example code (AtleteRepositoryTest, TeamRepositoryTest and TestcontainersDatajpatestApplicationTests).
To avoid the repetition of declaring the PostgreSQL container and the dynamic properties, I tried the following:
JUnit 5 extension
Baeldung has a blog about how you can use a JUnit 5 extension to avoid the duplication.
import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.BeforeAllCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.testcontainers.containers.PostgreSQLContainer; public class PostgreSQLExtension implements BeforeAllCallback, AfterAllCallback { private PostgreSQLContainer<?> postgres; @Override public void beforeAll(ExtensionContext context) { postgres = new PostgreSQLContainer<>("postgres:11"); postgres.start(); System.setProperty("spring.datasource.url", postgres.getJdbcUrl()); System.setProperty("spring.datasource.username", postgres.getUsername()); System.setProperty("spring.datasource.password", postgres.getPassword()); } @Override public void afterAll(ExtensionContext context) { postgres.stop(); } }
It works if you only have 1 test, but not if you run multiple at the same time (using IntelliJ or with Maven). In that case, one of the tests will fail because there is no connection with the database that can be made.
Also note that this extension does not use the DynamicPropertyRegistry
, but plain environment variables.
See the feature/testcontainers-datajpatest_baeldung-extension branch for the code.
Using a common superclass
On the branch feature/testcontainers-datajpatest_database-base-test, I tried using a common superclass:
import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; import org.testcontainers.containers.PostgreSQLContainer; public class DatabaseBaseTest { private static final PostgreSQLContainer<?> CONTAINER = new PostgreSQLContainer<>("postgres:11"); @BeforeAll static void start() { CONTAINER.start(); } @AfterAll static void stop() { CONTAINER.stop(); } @DynamicPropertySource static void registerProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", () -> { String jdbcUrl = CONTAINER.getJdbcUrl(); System.out.println("jdbcUrl = " + jdbcUrl); return jdbcUrl; }); registry.add("spring.datasource.username", CONTAINER::getUsername); registry.add("spring.datasource.password", CONTAINER::getPassword); } }
Unfortunately that also does not work. I noticed in the logging that the @DynamicPropertySource
annotated method was only called once and not for each test, which led me to try option 3:
Common superclass with @DynamicPropertySource
in subclasses
When using the common superclass, but adding the @DynamicPropertySource
method in each subclass, it works again.
Example code of such a subclass:
@DataJpaTest @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) public class AtleteRepositoryTest extends DatabaseBaseTest { @DynamicPropertySource static void registerProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", () -> { String jdbcUrl = CONTAINER.getJdbcUrl(); System.out.println("jdbcUrl = " + jdbcUrl); return jdbcUrl; }); registry.add("spring.datasource.username", CONTAINER::getUsername); registry.add("spring.datasource.password", CONTAINER::getPassword); } @Autowired private AtleteRepository repository; @Test void testSave() { repository.save(new Atlete("Wout Van Aert", 0, 1, 0)); assertThat(repository.count()).isEqualTo(1); } }
See branch feature/testcontainers-datajpatest_database-base-test_subclasses for that version.
So while it works, there is still a lot of duplication in each test class.
Are there any other options for avoiding the duplication?
Advertisement
Answer
To avoid Testcontainers code repetition I generally follow 2 approaches:
- Using ApplicationContextInitializer with @ContextConfiguration
import lombok.extern.slf4j.Slf4j; import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; import org.testcontainers.containers.PostgreSQLContainer; @Slf4j public class PostgreSQLContainerInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { private static PostgreSQLContainer sqlContainer = new PostgreSQLContainer("postgres:10.7"); static { sqlContainer.start(); } public void initialize (ConfigurableApplicationContext configurableApplicationContext){ TestPropertyValues.of( "spring.datasource.url=" + sqlContainer.getJdbcUrl(), "spring.datasource.username=" + sqlContainer.getUsername(), "spring.datasource.password=" + sqlContainer.getPassword() ).applyTo(configurableApplicationContext.getEnvironment()); } }
import com.sivalabs.myservice.common.PostgreSQLContainerInitializer; import com.sivalabs.myservice.entities.User; 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.test.context.ContextConfiguration; import javax.persistence.EntityManager; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; @DataJpaTest @AutoConfigureTestDatabase(replace= AutoConfigureTestDatabase.Replace.NONE) @ContextConfiguration(initializers = {PostgreSQLContainerInitializer.class}) class UserRepositoryTest { @Autowired EntityManager entityManager; @Autowired private UserRepository userRepository; @Test void shouldReturnUserGivenValidCredentials() { User user = new User(null, "test@gmail.com", "test", "Test"); entityManager.persist(user); Optional<User> userOptional = userRepository.login("test@gmail.com", "test"); assertThat(userOptional).isNotEmpty(); } }
- Using @DynamicPropertySource in Java 8+ Interface
import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; import org.testcontainers.containers.PostgreSQLContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @Testcontainers public interface PostgreSQLContainerInitializer { @Container PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:12.3"); @DynamicPropertySource static void registerPgProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", postgres::getJdbcUrl); registry.add("spring.datasource.username", postgres::getUsername); registry.add("spring.datasource.password", postgres::getPassword); } }
@DataJpaTest @AutoConfigureTestDatabase(replace= AutoConfigureTestDatabase.Replace.NONE) class UserRepositoryTest implements PostgreSQLContainerInitializer { .... .... }
With these approaches we don’t have to repeat PostgreSQLContainer declarations and Spring property settings.
Whether to use PostgreSQLContainer as a static field or not depends on whether you want to spin up a new container for every test or 1 container per test class.
PS: I avoided using common base class approach because sometime one test needs only 1 container and another test needs multiple containers. If we follow add all the containers in common base class then for every test/class all those containers will be started irrespective of their usage which makes tests very slow.