Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -292,3 +292,119 @@ locale or time zone need to be annotated with the respective annotation:

Tests annotated in this way will never execute in parallel with tests annotated with
`@DefaultLocale` or `@DefaultTimeZone`.

[[writing-tests-built-in-extensions-SystemProperty]]
==== `@ClearSystemProperty` and `@SetSystemProperty`

The `@ClearSystemProperty` and `@SetSystemProperty` annotations can be used to clear and set, respectively, the values of system properties for a test execution.
Both annotations work on the test method and class level, are repeatable, combinable, and inherited from higher-level containers.
After the annotated method has been executed, the properties mentioned in the annotation will be restored to their original value or the value of the higher-level container, or will be cleared if they didn't have one before.
Other system properties that are changed during the test, are *not* restored (unless the `@RestoreSystemProperties` is used).

For example, clearing a system property for a test execution can be done as follows:

[source,java,indent=0]
----
include::example$java/example/SystemPropertyExtensionDemo.java[tag=systemproperty_clear_simple]
----

And setting a system property for a test execution:

[source,java,indent=0]
----
include::example$java/example/SystemPropertyExtensionDemo.java[tag=systemproperty_set_simple]
----

As mentioned before, both annotations are repeatable, and they can also be combined:

[source,java,indent=0]
----
include::example$java/example/SystemPropertyExtensionDemo.java[tag=systemproperty_using_set_and_clear]

----

Note that class-level configurations are overwritten by method-level configurations:

[source,java,indent=0]
----
include::example$java/example/SystemPropertyExtensionDemo.java[tag=systemproperty_using_at_class_level]
----

[NOTE]
====
Method-level configurations are visible in both `@BeforeEach` setup methods and `@AfterEach` teardown methods (see <<extensions-execution-order-overview, user code and extension code execution order>>).

A class-level configuration means that the specified system properties are cleared/set before and reset after each individual test in the annotated class.
====

== `@RestoreSystemProperties`

`@RestoreSystemProperties` can be used to restore changes to system properties made directly in code.
While `@ClearSystemProperty` and `@SetSystemProperty` set or clear specific properties and values, they don't allow property values to be calculated or parameterized, thus there are times you may want to directly set properties in your test code.
`@RestoreSystemProperties` can be placed on test methods or test classes and will completely restore all system properties to their original state after a test or test class is complete.

In this example, `@RestoreSystemProperties` is used on a test method, ensuring any changes made in that method are restored:

[source,java,indent=0]
----
include::example$java/example/SystemPropertyExtensionDemo.java[tag=systemproperty_restore_test]
----

When `@RestoreSystemProperties` is used on a test class, any system properties changes made during the entire lifecycle of the test class, including test methods, `@BeforeAll`, `@BeforeEach` and 'after' methods, are restored after the test class' lifecycle is complete.
In addition, the annotation is inherited by each test method just as if each one was annotated with `@RestoreSystemProperties`.

In the following example, both test methods see the system property changes made in `@BeforeAll` and `@BeforeEach`, however, the test methods are isolated from each other (`isolatedTest2` does not 'see' changes made in `isolatedTest1`).
As shown in the second example below, the class-level `@RestoreSystemProperties` ensures that system property changes made within the annotated class are completely restored after the class's lifecycle, ensuring that changes are not visible to `SomeOtherTestClass`.
Note that `SomeOtherTestClass` uses the `@ReadsSystemProperty` annotation: This ensures that JUnit does not schedule the class to run during any test known to modify system properties (see <<Thread-Safety>>).

[source,java,indent=0]
----
include::example$java/example/SystemPropertyExtensionDemo.java[tag=systemproperty_class_restore_setup]
----

Some other test class, running later:

[source,java,indent=0]
----
include::example$java/example/SystemPropertyExtensionDemo.java[tag=systemproperty_class_restore_isolated_class]
----

== Using `@ClearSystemProperty`, `@SetSystemProperty`, and `@RestoreSystemProperties` together

All three annotations can be combined, which could be used when some system properties are parameterized (i.e. need to be set in code) and others are not.
For instance, imagine testing an image generation utility that takes configuration from system properties.
Basic configuration can be specified using `Set` and `Clear` and the image size parameterized:

[source,java,indent=0]
----
include::example$java/example/SystemPropertyExtensionDemo.java[tag=systemproperty_method_combine_all_test]
----

[NOTE]
====
Using `@RestoreSystemProperties` is not necessary to restore system properties modified via `@ClearSystemProperty` or `@SetSystemProperty` - they each automatically restore the referenced properties.
'Restore', is only needed if system properties are modified in some way _other than_ Clear and Set during a test.
====

=== `@RestoreSystemProperties` Limitations

The system `Properties` object is normally just a hashmap of strings, however, it is technically possible to store non-string values and create {jdk-javadoc-base-url}/java.base/java/util/Properties.html#%3Cinit%3E(java.util.Properties)[nested `Properties` with inherited default values].
`@RestoreSystemProperties` restores the original `Properties` object with all of its potential richness _after_ the annotated scope is complete.
However, for use during the test _within_ the test scope it provides a cloned `Properties` object with these limitations:

- Properties with non-string values are removed
- Nested `Properties` are flattened into a non-nested instance that has the same effective values, but not necessarily the same structure

== Thread-Safety

Since system properties are global state, reading and writing them during <<writing-tests-parallel-execution, parallel test execution>> can lead to unpredictable results and flaky tests.
The system property extension is prepared for that and tests annotated with `@ClearSystemProperty`, `@SetSystemProperty`, or `@RestoreSystemProperties` will never execute in parallel (thanks to https://docs.junit.org/current/api[resource locks]) to guarantee correct test results.

However, this does not cover all possible cases.
Tested code that reads or writes system properties _independently_ of the extension can still run in parallel to it and may thus behave erratically when, for example, it unexpectedly reads a property set by the extension in another thread.
Tests that cover code that reads or writes system properties need to be annotated with the respective annotation:

* `@ReadsSystemProperty`
* `@WritesSystemProperty` (though consider using `@RestoreSystemProperties` instead)

Tests annotated in this way will never execute in parallel with tests annotated with `@ClearSystemProperty`, `@SetSystemProperty`, or `@RestoreSystemProperties`.
153 changes: 153 additions & 0 deletions documentation/src/test/java/example/SystemPropertyExtensionDemo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* Copyright 2015-2025 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package example;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.ClassOrderer;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestClassOrder;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.util.ClearSystemProperty;
import org.junit.jupiter.api.util.ReadsSystemProperty;
import org.junit.jupiter.api.util.RestoreSystemProperties;
import org.junit.jupiter.api.util.SetSystemProperty;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

public class SystemPropertyExtensionDemo {

// tag::systemproperty_clear_simple[]
@Test
@ClearSystemProperty(key = "some property")
void testClearingProperty() {
assertThat(System.getProperty("some property")).isNull();
}
// end::systemproperty_clear_simple[]

// tag::systemproperty_set_simple[]
@Test
@SetSystemProperty(key = "some property", value = "new value")
void testSettingProperty() {
assertThat(System.getProperty("some property")).isEqualTo("new value");
}
// end::systemproperty_set_simple[]

// tag::systemproperty_using_set_and_clear[]
@Test
@ClearSystemProperty(key = "1st property")
@ClearSystemProperty(key = "2nd property")
@SetSystemProperty(key = "3rd property", value = "new value")
void testClearingAndSettingProperty() {
assertThat(System.getProperty("1st property")).isNull();
assertThat(System.getProperty("2nd property")).isNull();
assertThat(System.getProperty("3rd property")).isEqualTo("new value");
}
// end::systemproperty_using_set_and_clear[]

@Nested
// tag::systemproperty_using_at_class_level[]
@ClearSystemProperty(key = "some property")
class MySystemPropertyTest {

@Test
@SetSystemProperty(key = "some property", value = "new value")
void clearedAtClasslevel() {
assertThat(System.getProperty("some property")).isEqualTo("new value");
}

}
// end::systemproperty_using_at_class_level[]

// tag::systemproperty_restore_test[]
@ParameterizedTest
@ValueSource(strings = { "foo", "bar" })
@RestoreSystemProperties
void parameterizedTest(String value) {
System.setProperty("some parameterized property", value);
System.setProperty("some other dynamic property", "my code calculates somehow");
}
// end::systemproperty_restore_test[]

@Nested
@TestClassOrder(ClassOrderer.OrderAnnotation.class)
class SystemPropertyRestoreExample {

@Nested
@Order(1)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
// tag::systemproperty_class_restore_setup[]
@RestoreSystemProperties
class MySystemPropertyRestoreTest {

@BeforeAll
void beforeAll() {
System.setProperty("A", "A value");
}

@BeforeEach
void beforeEach() {
System.setProperty("B", "B value");
}

@Test
void isolatedTest1() {
System.setProperty("C", "C value");
}

@Test
void isolatedTest2() {
assertThat(System.getProperty("A")).isEqualTo("A value");
assertThat(System.getProperty("B")).isEqualTo("B value");

// Class-level @RestoreSystemProperties restores "C" to original state
assertThat(System.getProperty("C")).isNull();
}

}
// end::systemproperty_class_restore_setup[]

@Nested
@Order(2)
// tag::systemproperty_class_restore_isolated_class[]
@ReadsSystemProperty
class SomeOtherTestClass {

@Test
void isolatedTest() {
assertThat(System.getProperty("A")).isNull();
assertThat(System.getProperty("B")).isNull();
assertThat(System.getProperty("C")).isNull();
}

}

// end::systemproperty_class_restore_isolated_class[]
}

// tag::systemproperty_method_combine_all_test[]
@ParameterizedTest
@ValueSource(ints = { 100, 500, 1000 })
@RestoreSystemProperties
@SetSystemProperty(key = "DISABLE_CACHE", value = "TRUE")
@ClearSystemProperty(key = "COPYWRITE_OVERLAY_TEXT")
void imageGenerationTest(int imageSize) {
System.setProperty("IMAGE_SIZE", String.valueOf(imageSize)); // Requires restore

// Test your image generation utility with the current system properties
}
// end::systemproperty_method_combine_all_test[]

}
Loading
Loading