CodeGym /Courses /Spring Test /Fallback path: @DynamicPropertySource

Fallback path: @DynamicPropertySource

Spring Test
Level 25, Lesson 2
Available

1. The role of @DynamicPropertySource and @ServiceConnection

If you have just fallen in love with @ServiceConnection with all your heart, I understand you: less code, fewer errors, fewer chances to forget the password by accident. But tests have a peculiarity: part of the configuration becomes known only at runtime. The container starts PostgreSQL on a random port, generates connection parameters, and these values need to be “injected” into Spring before it creates the datasource.

So the question is no longer whether you need the container at all. For a regular PostgreSQL datasource, @ServiceConnection remains the first choice. @DynamicPropertySource is needed when automatic wiring is no longer enough and runtime values have to be placed into the Environment manually.

In an ideal world, Spring Boot would pick everything up on its own. And in the typical PostgreSQL case, it really does this through a service connection. But there are situations where automatic wiring does not work or does not fit: you need nonstandard property keys, you want to connect the container not to the datasource but to another set of settings, or you need to manually put several additional dynamic values into the Environment. Then you have to return to the more universal Spring Test mechanism — @DynamicPropertySource.

It is useful to think about it like this: @ServiceConnection is “the fast lane on the highway,” while @DynamicPropertySource is “the dirt road” that leads to the same place, but lets you go around almost any roadblock. The dirt road is longer and messier (the code is bulkier), but you control everything yourself.

2. How @DynamicPropertySource works

When you hear “dynamic property source,” you might think that magic, rituals, and a Spring summoning are about to happen. In reality, the idea is very down to earth: Spring Test gives you a point where you can programmatically add properties to the Spring Environment right before building the ApplicationContext. That is, not through application-test.yml, not through properties = ... on the annotation, but through code.

Technically, it looks like this. In the test class, you declare a static method and mark it with @DynamicPropertySource. Spring will find this method while preparing the test context and call it, passing in a DynamicPropertyRegistry object. Then, in that registry, you add pairs of “key → value supplier.” And here is the important nuance: you do not have to provide the value immediately. It is much more convenient to provide a supplier (Supplier), because the container may start a bit later, and then postgres::getJdbcUrl will be evaluated at the right moment.

Below is the diagram (a bit simplified, but honest in terms of meaning) of what happens when we connect Testcontainers through @DynamicPropertySource:

flowchart TD
    A["JUnit 6 starts executing the test class"] --> B["Testcontainers prepares the container (PostgreSQLContainer)"]
    B --> C["Spring Test prepares the ApplicationContext"]
    C --> D["@DynamicPropertySource registers dynamic properties in the DynamicPropertyRegistry"]
    D --> E["Spring binds the properties (Environment)"]
    E --> F["DataSource / Flyway / JPA infrastructure is created"]
    F --> G["The tests start and work with a real PostgreSQL"]

Now about the requirements that most often “bite” beginners:

The method with @DynamicPropertySource must be static. This is not a whim, but a direct consequence of the fact that properties must be registered before the test class instance is created. If the method is not static, Spring simply will not be able to call it safely during bootstrap, and you will get a “method must be static” level error.

DynamicPropertyRegistry is not an “application settings map,” but specifically a registry of dynamic values. The best style is to add only what really depends on runtime there: URL, port, login/password, and temporary addresses. If you start putting everything there, including constants, you very quickly turn @DynamicPropertySource into a dumping ground (and then wonder why nobody wants to touch your test code without a hard hat).

3. An @DataJpaTest example with Postgres

Now we will build a minimal, “readable by eye” example for ContentHub. We need an @DataJpaTest that runs against PostgreSQL in a container. The main thing in this scenario is not the test itself, but the wiring: the container must start, and Spring must receive the correct spring.datasource.* properties.

To avoid building a second separate world, we will take the same ArticleRepositoryPostgresContainerTest as in the preferred-path variant. Only the way the spring.datasource.* properties reach the context changes.

import javax.sql.DataSource;

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;

@Testcontainers // Enable Testcontainers integration with the test runner
@DataJpaTest // Data-slice test: we start only the JPA part of the context
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) // Do not replace the DataSource with an embedded database
class ArticleRepositoryPostgresContainerTest {

    @Container // The container is managed by the Testcontainers lifecycle
    static PostgreSQLContainer
   postgres =
            new PostgreSQLContainer<>("postgres:16-alpine"); // PostgreSQL image for tests

    @DynamicPropertySource
    static void postgresProperties(DynamicPropertyRegistry registry) {
        // Important: pass a Supplier so the value is evaluated when the container is already ready
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired
    private DataSource dataSource;

    @Test // JUnit 6
    void connectsToPostgres() throws Exception {
        // We are checking the real connection: if this fails, the wiring did not happen
        try (var c = dataSource.getConnection()) {
            assertThat(c.isValid(1)).isTrue(); // 1 second timeout for the check
        }
    }
}

Pay attention to two things. First, replace = NONE remains in place: @DynamicPropertySource does not cancel the data-slice rules, it just replaces the automatic bridge with manual property registration. Second, this is the same test shape as with @ServiceConnection: the container is still static, the class itself is still @DataJpaTest, only the datasource wiring changes.

If you want a bit more peace of mind around the lifecycle, keep a simple rule: do not evaluate values too early. postgres::getJdbcUrl is safer than trying to pull out the string in advance and save it “somewhere nearby.”

Additional dynamic properties

At first, it seems that the datasource is everything. But a real project likes to add “second axes.” For example, you have Flyway enabled, and it may use separate settings that differ from the datasource. Or you want SQL logging to be enabled only in a container-based subset. Or you need to inject a dynamic port into an integration client — and all of this is conveniently done with the same mechanism.

Below is not a new class, but a replacement variant for the postgresProperties method inside the same ArticleRepositoryPostgresContainerTest. In most projects, Flyway will take settings from the datasource anyway, so this is not an always-on template. But it demonstrates the idea well: DynamicPropertyRegistry can populate any keys, not just spring.datasource.*.

// Replacement variant for the postgresProperties method inside the same ArticleRepositoryPostgresContainerTest
@DynamicPropertySource
static void postgresAndFlyway(DynamicPropertyRegistry registry) {
    // Base DataSource settings for Spring Boot
    registry.add("spring.datasource.url", postgres::getJdbcUrl);
    registry.add("spring.datasource.username", postgres::getUsername);
    registry.add("spring.datasource.password", postgres::getPassword);

    // Explicitly pass the same parameters to Flyway (sometimes useful for nonstandard scenarios)
    registry.add("spring.flyway.url", postgres::getJdbcUrl);
    registry.add("spring.flyway.user", postgres::getUsername);
    registry.add("spring.flyway.password", postgres::getPassword);
}

Here it is important not to go to the extreme of “since there is a DynamicPropertyRegistry, I will put half of application.yml into it.” It is better to keep a simple rule: dynamic means dependent on runtime. If a value is static, it is easier and more honest to set it through application-test.yml or through properties = ... on the test annotation. That way you will preserve readability and not turn dynamic properties into a “secret place where all settings live.”

4. @ServiceConnection vs @DynamicPropertySource

Of course, one might want to declare @DynamicPropertySource “obsolete” because @ServiceConnection exists. But that would be untrue: they have different goals. @ServiceConnection gives you a short path, but works best in standard scenarios and for supported containers/connection types. @DynamicPropertySource is a more universal screwdriver: it may not be as elegant, but it fits almost always.

@ServiceConnection remains the first choice for the usual PostgreSQL datasource path. @DynamicPropertySource is needed exactly when the standard bridge is no longer enough.

Let us compare them in a table — it is easier to remember than ten paragraphs of “in general, it is kind of like this”:

Criterion @ServiceConnection @DynamicPropertySource
Code size Usually minimal Usually more boilerplate
Readability High: “container → service connection” Medium: you need to understand property keys
Flexibility Good as long as you stay within the “standard” path Very high: you can register any properties
Nonstandard keys Uncomfortable/impossible without extra bridges Exactly what it is made for
Copy-paste risk Low Higher (if you repeat registry.add in every test)
Main status in the course Preferred path Fallback path

If you work with ContentHub and need a regular PostgreSQL datasource in a test, your first choice is @ServiceConnection. But if you suddenly find out that part of the application configuration is “picked up” via its own custom keys (or Boot cannot connect your container to the right subsystem by itself), @DynamicPropertySource becomes an honest and understandable solution. The only important thing to remember is this: a fallback tool does not mean “bad.” It means “more manual” — and therefore it requires more discipline.

5. How to avoid copy-paste

@DynamicPropertySource has one side effect: it encourages copy-paste. The first test class is fine. The second makes you want to “just copy it quickly.” The third, and suddenly you realize that a new kind of technical debt has appeared in the project: “dynamic properties are spread across tests.”

The simplest remedy is to move the property mapping into a small helper. The important thing is that this helper should not turn into a huge “test framework named after yourself” that can do everything except explain what is happening.

Here is an example of a minimalist helper class: it does not hide the container and does not manage its lifecycle, but only helps register keys in a consistent way.

import org.springframework.test.context.DynamicPropertyRegistry;
import org.testcontainers.containers.PostgreSQLContainer;

final class PostgresDynamicProperties {

    static void register(DynamicPropertyRegistry registry, PostgreSQLContainer
   postgres) {
        // Register only what really changes from one run to another (runtime values)
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    private PostgresDynamicProperties() {
        // Utility class: no instances needed
    }
}

And in the test class, there remains an honest “entry point” where you can see what is happening:

import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;

@DynamicPropertySource
static void postgresProps(DynamicPropertyRegistry registry) {
    // In the test, it is clear: we are simply passing the container values into the standard Spring Boot keys
    PostgresDynamicProperties.register(registry, postgres);
}

Why is this a good compromise for our course? Because our goal is not to build ultra-universal infrastructure, but to keep the container-based subset small and understandable. We want to remove repetitive lines, but we do not want the student to read the test and think: “wait, who is setting up the datasource? where? why does it work?” The helper saves lines and keeps the meaning close to the test.

6. Common mistakes with @DynamicPropertySource

Mistake #1: the method with @DynamicPropertySource is not static.
This is a classic: a student writes a nice void postgresProperties(...) method, and Spring responds rather harshly: “no.” Dynamic properties are needed before the test instance is created, so the method must be static. This is not a “version nuance,” but a conceptual requirement.

Mistake #2: they register a “value” instead of a Supplier, and accidentally evaluate it too early.
Sometimes you want to do the “simpler” thing: call postgres.getJdbcUrl() and pass the string. First, the registry API is built around Supplier. Second, even if you work around this with a lambda, early evaluation may happen before the container starts and lead to very strange failures. The safest style is method references: postgres::getJdbcUrl, postgres::getUsername, postgres::getPassword.

Mistake #3: they use both @ServiceConnection and @DynamicPropertySource for the same datasource at the same time.
This is the case where you try to put a seatbelt on top of an already worn seatbelt and accidentally buckle yourself to the door. If @ServiceConnection is already on the container, do not manually register the same spring.datasource.* keys — you increase the risk of a conflict and make the test less predictable. Choose one path for a test class.

Mistake #4: they dump everything into DynamicPropertyRegistry, including static settings.
When the registry turns into an “alternative application-test.yml,” the test becomes unreadable: important runtime-dependent values get lost among constants. Static things (for example, logging.level.*) are better kept in YAML or through local overrides in annotations. The dynamic registry is for what is truly dynamic.

Mistake #5: they forget about @AutoConfigureTestDatabase(replace = NONE) in @DataJpaTest and then get surprised.
The symptom is usually this: the container starts up nicely, you see PostgreSQL logs, but the tests behave as if there is no database there. The reason is that Spring Boot replaced the datasource with an embedded DB because you did not tell it “do not help.” In a container-backed @DataJpaTest, this is almost a mandatory line.

1
Task
Spring Test, level 25, lesson 2
Locked
Manual datasource registration through `@DynamicPropertySource`
Manual datasource registration through `@DynamicPropertySource`
1
Task
Spring Test, level 25, lesson 2
Locked
Move dynamic properties into a helper class
Move dynamic properties into a helper class
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION