1. Introduction
If you are used to thinking, “Well, properties are just strings, what could be so hard about that?”, then welcome to the world of tests, where a string suddenly turns into an adventure. Up to this point, we worked with configuration as something static: a YAML file, a couple of keys, and off we go. But sometimes a value appears only at test startup time — and that is normal; it is not a design error.
Imagine two typical scenarios from ContentHub (and really from any backend project). First: the attachment storage needs a path contenthub.storage.root, and you want it to be unique for a specific test run, so tests do not interfere with each other and do not fight over the same files. Second: you spin up a local moderation stub server that listens on a random free port (because a fixed port is always taken... especially when you are in a hurry). In both cases, you cannot pre-fill the exact value in YAML, because it will only be known “right now”.
And this is where the question comes up: how do you give Spring a property that is computed on the fly, but still behaves like an ordinary property under an ordinary key, for example contenthub.moderation.base-url? The answer is @DynamicPropertySource.
2. What @DynamicPropertySource is
It is important not to think of @DynamicPropertySource as “just another way to write properties, because we love annotations”. It is a specific Spring Test mechanism that lets you programmatically add or override property values in the test Environment of an ApplicationContext before the context is fully started and begins creating beans.
Put simply, in tests Spring does something like this: “First I will collect all configuration sources, build the final set of values, and only then will I start creating beans.” @DynamicPropertySource slots into exactly that “collect the final set of values” phase. So beans that read @Value("${contenthub.storage.root}") or @Value("${contenthub.moderation.base-url}") will see an ready-made dynamic value, as if it had been in YAML.
To anchor the moment in time, it is useful to picture it as a small pipeline:
flowchart TD
A[JUnit 6 starts the test] --> B[Spring TestContext prepares Environment]
B --> C[Loads application.yml + application-test.yml]
C --> D[Applies local overrides: properties / @TestPropertySource]
D --> E["@DynamicPropertySource adds runtime values"]
E --> F[ApplicationContext refresh + bean creation]
F --> G[Test executes]
The key takeaway: @DynamicPropertySource works not “after everything has already started”, but at the moment when Spring is still assembling the configuration. That makes it a powerful tool, but it also requires discipline: you are affecting the foundation on which the entire test context will stand.
3. Syntax and the static requirement
@DynamicPropertySource looks as if you “just wrote a method”, but under the hood Spring invokes it quite early. For this reason, the method must be static: at the moment when Spring assembles the context, an instance of your test class may not yet exist (and sometimes it is created later, while the context is already up and cached). So the rule is simple: if you see @DynamicPropertySource, accept static and move on.
Inside the method, you get a DynamicPropertyRegistry. It is a small “registry” where you add pairs of key -> supplier. Why a supplier, and not a value right away? Because Spring prefers laziness: the value can be computed later (or several times), and it should come out consistently.
Here is the minimal template:
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
class DemoTest {
@DynamicPropertySource
static void registerProps(DynamicPropertyRegistry registry) {
// Register the property before the context starts:
// that way, beans will see the value as if it were in application-test.yml.
registry.add("contenthub.storage.root", () -> "build/test-storage");
}
}
This example is intentionally “not dynamic”; it only shows the shape. In real life, you add not a constant, but a computed value. But the form is always the same: the property key and a Supplier that returns the value (usually a string). And yes: use the same keys that the application uses in normal configuration. @DynamicPropertySource is not a separate configuration reality; it is a way to feed a value into the same system.
4. Example: unique contenthub.storage.root
When you write tests around attachment logic, a file system is almost always involved. And a file system is an honest thing: if two tests write to the same directory, they will get in each other’s way without any philosophy. Sometimes that ends with “weird” failures that disappear on rerun. And sometimes it means you accidentally look into build/test-storage and find artifacts from tests that are weeks old (it is like archaeology, only without grants).
One simple approach is to give each test class its own unique storage directory. Then tests from different classes do not overlap, and it is also easier to understand who created what. And this is exactly where @DynamicPropertySource fits perfectly: we will compute the path, register it as the value of contenthub.storage.root, and then the application will work with that key as usual.
Below is a full minimal example: in one class we register the property, start the context, and verify the final value.
import java.nio.file.Path;
import java.util.UUID;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
@ActiveProfiles("test")
class DynamicStorageRootSpringBootTest {
// Compute the value once so it remains stable for the test class.
static final Path STORAGE_ROOT =
Path.of("build", "tmp", "contenthub-" + UUID.randomUUID());
@DynamicPropertySource
static void register(DynamicPropertyRegistry registry) {
// Register the runtime value before the context starts.
registry.add("contenthub.storage.root", () -> STORAGE_ROOT.toString());
}
@Value("${contenthub.storage.root}")
String root;
@Test
void readsDynamicStorageRoot() {
// For Spring, this is now just an ordinary property from the Environment.
assertThat(root).isEqualTo(STORAGE_ROOT.toString());
}
}
There are several important “why”s here. We keep the path in static final so that it is stable for this test class. We do not compute UUID every time in the supplier (otherwise Spring may get different values on different reads). And we use build/tmp/... because it is a “junk” build area that is usually safe to clean up and does not look like “oops, I just deleted my home directory”.
Pay attention to the verification form: for @Value("${contenthub.storage.root}"), this is now just an ordinary property from the Environment. The bean does not care whether it came from YAML or through @DynamicPropertySource. That is the point of the mechanism: the dynamic value is injected into the same configuration system, instead of living a separate hidden life.
And one more practical nuance: if your production-like application expects the directory to already exist, you can create it once in a static block or in a separate helper. But more often, storage services can create the directory on first use themselves, and that is good — less “manual magic” in the test.
5. Example: a dynamic base-url for a stub
With URLs for external services in tests, there is an annoying tradition: a fixed port is almost always already taken by somebody. Yesterday it was Docker, today it is another test, tomorrow it is your own browser with some extension that decided to become a server (yes, that happens too). So a good testing approach is to start the stub server on port 0, meaning “give me any free one”, and then find out which port was actually assigned.
The problem is that contenthub.moderation.base-url depends on that port. So the value cannot be written in advance into application-test.yml or into the properties attribute. It becomes known only after the stub server starts. And that is where @DynamicPropertySource once again turns out to be “the” tool.
Let us show this with a very down-to-earth example using JDK HttpServer. For now, we only need one fact: the server has started, and we know its real port.
import java.net.InetSocketAddress;
import com.sun.net.httpserver.HttpServer;
class ModerationStub {
static HttpServer start() {
try {
// Port 0 means "give me any free port".
var server = HttpServer.create(new InetSocketAddress(0), 0);
// In a real stub server, handlers are usually added here (createContext),
// but for the example we only care about the fact that it started and the port is available.
server.start();
return server;
} catch (Exception e) {
// In tests, it is often easier to "fail fast" than to drag checked exceptions around.
throw new RuntimeException(e);
}
}
}
Here we only need the wiring part: start the server as static final, take the port, and bind it to contenthub.moderation.base-url. The test around it will be structured the same way as in the storage.root example.
import com.sun.net.httpserver.HttpServer;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
class DynamicModerationUrlTest {
// The server must start before the Spring context starts,
// otherwise we will not know the port in time.
static final HttpServer server = ModerationStub.start();
// The port is known only after startup.
static final int port = server.getAddress().getPort();
@DynamicPropertySource
static void register(DynamicPropertyRegistry registry) {
// The key must match what the application actually reads.
registry.add("contenthub.moderation.base-url", () -> "http://localhost:" + port);
}
}
What matters here is the meaning, not “perfect engineering”. We need to learn the port before the context starts and register it as an ordinary property. The createContext(...) handlers and the assertions themselves depend on the specific integration scenario. In a real test, you would also add server shutdown, for example in @AfterAll, but the wiring around the port remains exactly this.
And one subtle point that often breaks expectations: do not try to start such a server in @BeforeAll and then “register properties” afterward. The Spring context is usually created before your @BeforeAll runs. So the correct approach is to start the stub in a way that makes the port known before the context is created: through static initialization or through a helper that is called when the class loads.
6. When to use @DynamicPropertySource
It is very easy to fall in love with @DynamicPropertySource and start “dynamically configuring everything”. It is a bit like discovering var in Java and then writing var even where it looks like a riddle. Technically possible, but the reader will be sad.
A good rule is this: dynamic properties are needed only when a value is truly determined at startup time and cannot be honestly written in advance. If a value is stable and the same for most tests, it should live in application-test.yml. If a value only needs to be overridden in one or two tests, it is much simpler and clearer to write @SpringBootTest(properties = "...") or @TestPropertySource(properties = { "..." }). Dynamic behavior is an edge case, not the default style.
To reinforce this as an engineering decision, not just a feeling, here is a table that helps you choose the tool:
| Test situation | What to choose | Why it makes sense |
|---|---|---|
| The value is the same for almost all tests | application-test.yml | One obvious baseline, minimal surprises |
| You need to change 1–2 keys in only one class | @SpringBootTest(properties = ...) | The override is visible right next to the test |
| You need a set of related overrides for one class | @TestPropertySource(properties = {...}) | In one place, readable, no separate file |
| The value appears only after a resource is computed/started | @DynamicPropertySource | Otherwise you simply do not know the value in advance |
There is also another practical aspect that we already touched on when talking about the cost of Spring tests and context caching. Dynamic properties can multiply contexts if you use them carelessly. For example, if you generate a new UUID for contenthub.storage.root in every test class, then each such class may require a separate ApplicationContext. Sometimes that is fine (when there are few tests and isolation matters), but sometimes it turns into “why did the tests start taking 4 minutes instead of 40 seconds”.
So keep a balance. If you need uniqueness, let it exist where it protects against a real risk (files, ports, temporary directories). But do not make “dynamic” something that lives happily in YAML.
7. Typical mistakes when working with @DynamicPropertySource
Almost all problems with @DynamicPropertySource happen not because the annotation is “bad”, but because it gives you influence over an early phase of the context life cycle, and that influence is easy to abuse. Below is a set of mistakes that newcomers encounter most often, and that it is nice to learn to avoid before they become your personal collection of weird CI failures.
Mistake #1: use @DynamicPropertySource for constant values.
If the value is known in advance and stable, the dynamic mechanism makes the test less readable. As a result, the next person (and you in a week) will look for “where is this set?”, instead of just opening application-test.yml. Dynamic behavior should be justified by the fact that the value really appears at runtime.
Mistake #2: the method is not static (or has the wrong signature).
@DynamicPropertySource expects a static method that takes a DynamicPropertyRegistry. If you make it non-static, Spring simply will not be able to invoke the registration at the right moment. The error will look like “why were my properties not applied?”, and you will spend time reading logs instead of seeing the cause immediately.
Mistake #3: “randomness inside the supplier” — different values on every read.
It is very tempting to write registry.add("key", () -> UUID.randomUUID()). And that is almost guaranteed to lead to chaos, because the same property may be read several times at different stages. The property must be stable: compute the value once in static final, and just return it from the supplier.
Mistake #4: heavy logic and side effects right inside the registration method.
Property registration should be as short as possible: add the keys, hand over the suppliers, leave. If you start spinning up a server, writing files, making network calls, or doing any kind of “life” inside the method, you will get tests that are hard to debug and that break for no obvious reason. If a resource needs to be started in advance, do it in a separate static initializer or in a small helper class, and in the registration method only bind values to keys.
Mistake #5: register the wrong key (the one the application does not read).
Sometimes the test author registers something like contenthub.moderation.url, while the application actually uses contenthub.moderation.base-url. As a result, the test looks “configured”, but the application continues to use the default from YAML. This is especially tricky because the test may even pass — just not by the path you thought. The best cure is to keep property naming consistent and not invent keys on the fly.
GO TO FULL VERSION