My objective is to demonstrate a sample test for a JPA based Spring Boot Application using TestContainers. The sample is based on an example at the TestContainer github repo.
Sample App
The Spring Boot based application is straightforward - It is a Spring Data JPA based application with the web layer written using Spring Web Flux. The entire sample is available at my github repo and it may be easier to just follow the code directly there.
The City entity being persisted looks like this (using Kotlin):
import javax.persistence.Entity import javax.persistence.GeneratedValue import javax.persistence.Id @Entity data class City( @Id @GeneratedValue var id: Long? = null, val name: String, val country: String, val pop: Long ) { constructor() : this(id = null, name = "", country = "", pop = 0L) }
All that is needed to provide a repository to manage this entity is the following interface, thanks to the excellent Spring Data JPA project:
import org.springframework.data.jpa.repository.JpaRepository import samples.geo.domain.City interface CityRepo: JpaRepository<City, Long>
I will not cover the web layer here as it is not relevant to the discussion.
Testing the Repository
Spring Boot provides a feature called the Slice tests which is a neat way to test different horizontal slices of the application. A test for the CityRepo repository looks like this:import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.test.context.junit4.SpringRunner; import samples.geo.domain.City; import samples.geo.repo.CityRepo; import static org.assertj.core.api.Assertions.assertThat; @RunWith(SpringRunner.class) @DataJpaTest public class CitiesWithEmbeddedDbTest { @Autowired private CityRepo cityRepo; @Test public void testWithDb() { City city1 = cityRepo.save(new City(null, "city1", "USA", 20000L)); City city2 = cityRepo.save(new City(null, "city2", "USA", 40000L)); assertThat(city1) .matches(c -> c.getId() != null && c.getName() == "city1" && c.getPop() == 20000L); assertThat(city2) .matches(c -> c.getId() != null && c.getName() == "city2" && c.getPop() == 40000L); assertThat(cityRepo.findAll()).containsExactly(city1, city2); } }
The "@DataJpaTest" annotation starts up an embedded h2 databases, configures JPA and loads up any Spring Data JPA repositories(CityRepo in this instance).
This kind of a test works well, considering that JPA provides the database abstraction and if JPA is used correctly the code should be portable across any supported databases. However, assuming that this application is expected to be run against a PostgreSQL in production, ideally, there would be some level of integration testing done against the database, which is where TestContainer fits in. It provides a way to boot up PostgreSQL as a docker container.
TestContainers
The same repository test using TestContainers looks like this:import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; import org.testcontainers.containers.PostgreSQLContainer; import samples.geo.domain.City; import samples.geo.repo.CityRepo; import java.time.Duration; import static org.assertj.core.api.Assertions.assertThat; @RunWith(SpringRunner.class) @DataJpaTest @ContextConfiguration(initializers = {CitiesWithPostgresContainerTest.Initializer.class}) public class CitiesWithPostgresContainerTest { @ClassRule public static PostgreSQLContainer postgreSQLContainer = (PostgreSQLContainer) new PostgreSQLContainer("postgres:10.4") .withDatabaseName("sampledb") .withUsername("sampleuser") .withPassword("samplepwd") .withStartupTimeout(Duration.ofSeconds(600)); @Autowired private CityRepo cityRepo; @Test public void testWithDb() { City city1 = cityRepo.save(new City(null, "city1", "USA", 20000L)); City city2 = cityRepo.save(new City(null, "city2", "USA", 40000L)); assertThat(city1) .matches(c -> c.getId() != null && c.getName() == "city1" && c.getPop() == 20000L); assertThat(city2) .matches(c -> c.getId() != null && c.getName() == "city2" && c.getPop() == 40000L); assertThat(cityRepo.findAll()).containsExactly(city1, city2); } static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { public void initialize(ConfigurableApplicationContext configurableApplicationContext) { TestPropertyValues.of( "spring.datasource.url=" + postgreSQLContainer.getJdbcUrl(), "spring.datasource.username=" + postgreSQLContainer.getUsername(), "spring.datasource.password=" + postgreSQLContainer.getPassword() ).applyTo(configurableApplicationContext.getEnvironment()); } } }
The core of the code looks same as the previous test, but the repository here is being tested against a real PostgreSQL database here. To go into a little more detail -
A PostgreSQL container is being started up using a JUnit Class Rule which gets triggered before any of the tests are run. This dependency is being pulled in using a gradle dependency of the following type:
testCompile("org.testcontainers:postgresql:1.7.3")
The class rule starts up a PostgreSQL docker container(postgres:10.4) and configures a database, and credentials for the database. Now from Spring Boot's perspective, these details need to be passed on the application as properties BEFORE Spring starts creating a test context for the test to run in, and this is done for the test using an ApplicationContextInitializer, this is invoked by Spring very early in the lifecycle of a Spring Context.
The custom ApplicationContextInitializer which sets the database name, url and user credentials is hooked up to the test using this code:
... import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; ... @RunWith(SpringRunner.class) @DataJpaTest @ContextConfiguration(initializers = {CitiesWithPostgresContainerTest.Initializer.class}) public class CitiesWithPostgresContainerTest { ...
With this boiler plate set up in place TestContainer and Spring Boot slice test will take over running of the test. More importantly TestContainers also takes care of tear down, the JUnit Class Rule ensures that once the test is complete the containers are stopped and removed.