One JUnit 5 test calls System.setProperty, another reads that property, and a third changes the JVM default time zone with TimeZone.setDefault. Under parallel execution, how do you stop them corrupting each other, and what built-in keys does JUnit provide for cases like these?
answer
- Resources.SYSTEM_PROPERTIES / SYSTEM_OUT / SYSTEM_ERR / LOCALE / TIME_ZONE
- keys are strings — the constant is the shared vocabulary
- writer READ_WRITE, reader READ
- different key ⇒ still parallel
- restore the old value; locking is not restoring
basics
~20 sDeclare the shared JVM state with @ResourceLock using the constants in org.junit.jupiter.api.parallel.Resources — SYSTEM_PROPERTIES for the property tests, TIME_ZONE for the time-zone test — with READ_WRITE on the mutators and READ on the observer. Because these are canonical keys, tests written elsewhere coordinate with yours.
solid answer
~50 sTwo independent decisions: which key, and which mode. **Key.** JUnit ships canonical keys in `org.junit.jupiter.api.parallel.Resources`: `SYSTEM_PROPERTIES`, `SYSTEM_OUT`, `SYSTEM_ERR`, `LOCALE`, `TIME_ZONE`. They matter because a lock key is just a string with no meaning to JUnit — two tests coordinate only if they spell it identically. Using the shared constant means a test another team writes next year still interlocks with yours; a private `"sysprops"` literal protects nothing. **Mode.** The writer of the property declares `READ_WRITE`, the reader declares `READ`, so several readers overlap but never overlap the writer. The time-zone test declares `READ_WRITE` on `TIME_ZONE` — a different key, so it still runs in parallel with the property tests. For application resources, define your own constants in one shared class rather than scattering literals, since a typo silently disables the protection. And the better fix, where possible, is to stop mutating JVM globals at all — inject the clock, zone or configuration instead.
code
java · 38 linesimport org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.ResourceAccessMode;
import org.junit.jupiter.api.parallel.ResourceLock;
import org.junit.jupiter.api.parallel.Resources;
class GlobalStateTest {
@Test
@ResourceLock(value = Resources.SYSTEM_PROPERTIES, mode = ResourceAccessMode.READ_WRITE)
void writesProperty() {
String previous = System.getProperty("app.mode");
System.setProperty("app.mode", "maintenance");
try {
assertTrue(App.inMaintenance());
} finally {
if (previous == null) System.clearProperty("app.mode");
else System.setProperty("app.mode", previous);
}
}
@Test
@ResourceLock(value = Resources.SYSTEM_PROPERTIES, mode = ResourceAccessMode.READ)
void readsProperty() {
assertNotNull(System.getProperty("java.version"));
}
@Test
@ResourceLock(value = Resources.TIME_ZONE, mode = ResourceAccessMode.READ_WRITE)
void changesDefaultZone() {
TimeZone previous = TimeZone.getDefault();
TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
try {
assertEquals("UTC", Clock.zoneLabel());
} finally {
TimeZone.setDefault(previous);
}
}
}go deeper
Name the Resources constants and say that the mutating tests take READ_WRITE while pure readers take READ.
Explain that keys are matched as strings, so the canonical constants are what makes the protection interoperable, and that different keys keep unrelated tests parallel.
Add restoring previous values, correct key granularity for application resources, locking the narrowest node, and naming the refactor that would remove the need for a lock.
Set the policy: a shared constants class for application resources, review pressure against new JVM-global mutation, and dependency injection of clock/locale/configuration so tests never need these locks.
## Why these particular resources are hard System properties, the default locale, the default time zone and the standard output stream are all *process-wide singletons*. There is exactly one of each per JVM and any test can change it for every other test running at that moment. Sequentially this is merely untidy — a test sets a property, uses it and clears it. Under parallel execution it is a race: another test reading the property while the first is between set and clear observes whatever the mutator left there. This category is worth recognising as a class rather than as individual bugs. The tell is any call of the form `X.setDefault(...)` or `System.setSomething(...)` in a test. ## The canonical keys `org.junit.jupiter.api.parallel.Resources` exists to solve the coordination problem. A lock key is an arbitrary string; JUnit never inspects what it refers to. Two tests are only serialised if their key strings are equal. So if one team writes `@ResourceLock("system-properties")` and another writes `@ResourceLock("sysprops")`, both tests believe they are protected and neither is. The constants provide a shared vocabulary for the JVM-wide resources that everyone eventually touches: - `Resources.SYSTEM_PROPERTIES` — anything reading or writing `System.getProperty`/`setProperty`/`clearProperty`. - `Resources.SYSTEM_OUT` and `Resources.SYSTEM_ERR` — tests that redirect or capture the standard streams. - `Resources.LOCALE` — `Locale.setDefault` and anything whose formatting depends on the default locale. - `Resources.TIME_ZONE` — `TimeZone.setDefault` and default-zone-dependent formatting or parsing. - `Resources.GLOBAL` — the key underlying whole-suite exclusivity; you normally express that intent with the dedicated isolation annotation rather than by naming this key. Because the constants live in JUnit itself, a library's tests and your tests interlock correctly without any agreement between the authors. That is the entire value proposition, and it is why using the constant is strictly better than a private literal even inside a single codebase. ## Applying it to the three tests - The **property writer** mutates, so `@ResourceLock(value = Resources.SYSTEM_PROPERTIES, mode = ResourceAccessMode.READ_WRITE)`. - The **property reader** only observes, so `@ResourceLock(value = Resources.SYSTEM_PROPERTIES, mode = ResourceAccessMode.READ)`. Several such readers can run together; none runs while the writer holds the exclusive lock. - The **time-zone test** mutates a *different* resource, so `@ResourceLock(value = Resources.TIME_ZONE, mode = ResourceAccessMode.READ_WRITE)`. Different key means no contention with the property tests — they keep running in parallel, which is exactly the granularity you want. A test that mutates two of them declares two locks; the annotation is repeatable. The lock is held for the annotated node including its `@BeforeEach` and `@AfterEach`, which is essential here: the set/clear pair usually lives in setup and teardown, and it is that whole window that must be exclusive. ## Keys for your own resources For application-level shared state — a fixed port, a config file, a singleton cache, a database table — invent keys, but centralise them: ```java public final class TestResources { public static final String PAYMENTS_DB = "db.payments"; public static final String WIREMOCK_PORT_8089 = "port.8089"; private TestResources() {} } ``` A constant is greppable and a typo becomes a compile error rather than a silently missing lock. Choose granularity deliberately: one key per genuinely independent resource. Too coarse and unrelated tests queue behind each other; too fine and two tests that really do collide use different keys and are not protected. ## Restore what you change Locking makes the mutation safe with respect to *concurrent* tests, but it does nothing about *later* ones. A test that sets the default time zone and never restores it leaves every subsequent test running in that zone. Always capture the previous value and restore it in a `finally` block or teardown — and note that with parallelism the failure this causes will look random, because which tests ran afterwards is no longer deterministic. ## The better fix Every lock on a JVM global is a workaround for production code that reads a global instead of receiving a dependency. If the code under test took a `Clock`, a `ZoneId`, a `Locale` or a configuration object as a parameter, the test would need no lock, would run fully in parallel, and would be clearer to read. Locking is the right answer when the global is imposed on you — a third-party library that reads `System.getProperty` at class-initialisation time, or code you cannot change yet. Say that in an interview: the strongest answer names the lock *and* the refactoring that would make it unnecessary.
- Why use Resources.SYSTEM_PROPERTIES instead of your own string literal, if both are just strings?Because coordination is by exact string equality and the constant is the vocabulary everyone else uses. A private literal only protects tests inside your own codebase that spell it identically, and it does nothing against a library's tests or a colleague's new test that reaches for the canonical constant. The constant also makes typos impossible and the usage greppable.
- Your test locks Resources.TIME_ZONE, mutates the default zone, and never restores it. Is the suite safe?No. The lock protects only against tests running *concurrently* with it; tests that run afterwards inherit the changed default zone. Under parallel execution the resulting failures look random because the set of subsequent tests is no longer deterministic. Always capture and restore the previous value in a finally block or teardown, which is inside the locked window.
- Two tests write different, unrelated system properties. Do they really need to be serialised?Strictly no, but with the canonical key they will be, because the key is the same. That is usually the right trade: the canonical key is what makes the protection interoperable, and property writes are fast so the queue is short. Only if profiling shows that key is genuinely hot would I introduce finer keys per property name, accepting that the finer convention is unknown to other people's tests.
saying these in an interview costs you the question
- Assuming JUnit knows which resource a key refers to and detects conflicts automatically
- Inventing a private key such as 'sysprops' instead of the canonical constant
- Believing the lock also restores the mutated value after the test
- Declaring READ on a test whose setup mutates the resource
- Locking the class instead of just the methods that touch the global, and losing all parallelism within it