1. Overview
Today’s applications don’t live in isolation: we usually need to connect to various external components such as PostgreSQL, Apache Kafka, Cassandra, Redis, and other external APIs.
In this tutorial, we’re going to see how Spring Framework 5.2.5 facilitates testing such applications with the introduction of dynamic properties.
First, we’ll start by defining the problem and seeing how we used to solve the problem in a less than ideal way. Then, we’ll introduce the @DynamicPropertySource annotation and see how it offers a better solution to the same problem. In the end, we’ll also take a look at another solution from test frameworks that can be superior compared to pure Spring solutions.
2. The Problem: Dynamic Properties
Let’s suppose we’re developing a typical application that uses PostgreSQL as its database. We’ll begin with a simple JPA entity:
@Entity
@Table(name = "articles")
public class Article {
@Id
@GeneratedValue(strategy = IDENTITY)
private Long id;
private String title;
private String content;
// getters and setters
}
To make sure this entity works as expected, we should write a test for it to verify its database interactions. Since this test needs to talk to a real database, we should set up a PostgreSQL instance beforehand.
There are different approaches to set up such infrastructural tools during test executions. As a matter of fact, there are three main categories of such solutions:
- Set up a separate database server somewhere just for the tests
- Use some lightweight, test-specific alternatives or fakes such as H2
- Let the test itself manage the lifecycle of the database
As we shouldn’t differentiate between our test and production environments, there are better alternatives compared to using test doubles such as H2. The third option, in addition to working with a real database, offers better isolation for tests. Moreover, with technologies like Docker and Testcontainers, it’s easy to implement the third option.
Here’s what our test workflow will look like if we use technologies like Testcontainers:
- Set up a component such as PostgreSQL before all tests. Usually, these components listen to random ports.
- Run the tests.
- Tear down the component.
If our PostgreSQL container is going to listen to a random port every time, then we should somehow set and change the spring.datasource.url configuration property dynamically. Basically, each test should have its own version of that configuration property.
When the configurations are static, we can easily manage them using Spring Boot’s configuration management facility. However, when we’re facing dynamic configurations, the same task can be challenging.
Now that we know the problem, let’s see a traditional solution for it.
3. Traditional Solution
The first approach to implement dynamic properties is to use a custom ApplicationContextInitializer. Basically, we set up our infrastructure first and use the information from the first step to customize the ApplicationContext:
@SpringBootTest
@Testcontainers
@ContextConfiguration(initializers = ArticleTraditionalLiveTest.EnvInitializer.class)
class ArticleTraditionalLiveTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:11")
.withDatabaseName("prop")
.withUsername("postgres")
.withPassword("pass")
.withExposedPorts(5432);
static class EnvInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
TestPropertyValues.of(
String.format("spring.datasource.url=jdbc:postgresql://localhost:%d/prop", postgres.getFirstMappedPort()),
"spring.datasource.username=postgres",
"spring.datasource.password=pass"
).applyTo(applicationContext);
}
}
// omitted
}
Let’s walk through this somewhat complex setup. JUnit will create and start the container before anything else. After the container is ready, the Spring extension will call the initializer to apply the dynamic configuration to the Spring Environment. Clearly, this approach is a bit verbose and complicated.
Only after these steps can we write our test:
@Autowired
private ArticleRepository articleRepository;
@Test
void givenAnArticle_whenPersisted_thenShouldBeAbleToReadIt() {
Article article = new Article();
article.setTitle("A Guide to @DynamicPropertySource in Spring");
article.setContent("Today's applications...");
articleRepository.save(article);
Article persisted = articleRepository.findAll().get(0);
assertThat(persisted.getId()).isNotNull();
assertThat(persisted.getTitle()).isEqualTo("A Guide to @DynamicPropertySource in Spring");
assertThat(persisted.getContent()).isEqualTo("Today's applications...");
}
4. The @DynamicPropertySource
Spring Framework 5.2.5 introduced the @DynamicPropertySource annotation to facilitate adding properties with dynamic values. All we have to do is to create a static method annotated with @DynamicPropertySource and having just a single DynamicPropertyRegistry instance as the input:
@SpringBootTest
@Testcontainers
public class ArticleLiveTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:11")
.withDatabaseName("prop")
.withUsername("postgres")
.withPassword("pass")
.withExposedPorts(5432);
@DynamicPropertySource
static void registerPgProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url",
() -> String.format("jdbc:postgresql://localhost:%d/prop", postgres.getFirstMappedPort()));
registry.add("spring.datasource.username", () -> "postgres");
registry.add("spring.datasource.password", () -> "pass");
}
// tests are same as before
}
As shown above, we’re using the add(String, Supplier