Spring Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

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:

  1. Set up a component such as PostgreSQL before all tests. Usually, these components listen to random ports.
  2. Run the tests.
  3. 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<Object>) method on the given DynamicPropertyRegistry to add some properties to the Spring Environment. This approach is much cleaner compared to the initializer one we saw earlier. Please note that methods annotated with @DynamicPropertySource must be declared as static and must accept only one argument of type DynamicPropertyRegistry

Basically, the main motivation behind the @DynmicPropertySource annotation is to more easily facilitate something that was already possible. Although it was initially designed to work with Testcontainers, it's possible to use it wherever we need to work with dynamic configurations.

5. An Alternative: Test Fixtures

So far, in both approaches, the fixture setup and the test code are tightly intertwined. Sometimes, this tight coupling of two concerns complicates the test code, especially when we have multiple things to set up. Imagine what the infrastructure setup would look like if we were using PostgreSQL and Apache Kafka in a single test.

In addition to that, the infrastructure setup and applying dynamic configurations will be duplicated in all tests that need them.

To avoid these drawbacks, we can use test fixtures facilities that most testing frameworks provide. For instance, in JUnit 5, we can define an extension that starts a PostgreSQL instance before all tests, configures Spring Boot, and stops the PostgreSQL instance after running tests:

public class PostgreSQLExtension implements BeforeAllCallback, AfterAllCallback {

    private PostgreSQLContainer<?> postgres;

    @Override
    public void beforeAll(ExtensionContext context) {
        postgres = new PostgreSQLContainer<>("postgres:11")
          .withDatabaseName("prop")
          .withUsername("postgres")
          .withPassword("pass")
          .withExposedPorts(5432);

        postgres.start();
        String jdbcUrl = String.format("jdbc:postgresql://localhost:%d/prop", postgres.getFirstMappedPort());
        System.setProperty("spring.datasource.url", jdbcUrl);
        System.setProperty("spring.datasource.username", "postgres");
        System.setProperty("spring.datasource.password", "pass");
    }

    @Override
    public void afterAll(ExtensionContext context) {
        postgres.stop();
    }
}

Here, we're implementing AfterAllCallback and BeforeAllCallback to create a JUnit 5 extension. This way, JUnit 5 will execute the beforeAll() logic before running all the tests, and the logic in the afterAll() method after running the tests. With this approach, our test code will be as clean as:

@SpringBootTest
@ExtendWith(PostgreSQLExtension.class)
public class ArticleTestFixtureLiveTest {
    // just the test code
}

In addition to being more readable, we can easily reuse the same functionality just by adding the @ExtendWith(PostgreSQLExtension.class) annotation. There's no need to copy-paste the whole PostgreSQL setup everywhere we need it, as we did in the other two approaches.

6. Conclusion

In this tutorial, we first saw how hard can it be to test a Spring component that depends on something like a database. Then, we introduced three solutions for this problem, each improving upon what the previous solution had to offer.

As usual, all the examples are available over on GitHub.

Spring bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE
guest
0 Comments
Inline Feedbacks
View all comments