@ClearEnvironmentVariable and @SetEnvironmentVariable

The @ClearEnvironmentVariable and @SetEnvironmentVariable annotations can be used to clear and set, respectively, the values of environment variables 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 variables 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 environment variables that are changed during the test, are not restored (unless the @RestoreEnvironmentVariables is used).

Warning

Java considers environment variables to be immutable, which is why this extension uses reflection to change them. This requires the SecurityManager to allow modifications and may break on different operating systems and/or Java versions. Be aware that this is a fragile solution and consider finding a better one for your specific situation. For more details and workarounds on Java 17+, see Warnings for Reflective Access.

For example, clearing a environment variable for a test execution can be done as follows:

@Test
@ClearEnvironmentVariable(key = "some variable")
void testClear() {
    assertThat(System.getenv("some variable")).isNull();
}

And setting a environment variable for a test execution:

@Test
@SetEnvironmentVariable(key = "some variable", value = "new value")
void testSet() {
    assertThat(System.getenv("some variable")).isEqualTo("new value");
}

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

@Test
@ClearEnvironmentVariable(key = "1st variable")
@ClearEnvironmentVariable(key = "2nd variable")
@SetEnvironmentVariable(key = "3rd variable", value = "new value")
void testClearAndSet() {
    assertThat(System.getenv("1st variable")).isNull();
    assertThat(System.getenv("2nd variable")).isNull();
    assertThat(System.getenv("3rd variable")).isEqualTo("new value");
}

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

@ClearEnvironmentVariable(key = "some variable")
class MyEnvironmentVariableTest {

    @Test
    @SetEnvironmentVariable(key = "some variable", value = "new value")
    void clearedAtClasslevel() {
        assertThat(System.getenv("some variable")).isEqualTo("new value");
    }

}
Note

Method-level configurations are visible in both @BeforeEach setup methods and @AfterEach teardown methods (see user code and extension code execution order).

Since v1.7.0, a class-level configuration means that the specified environment variables are cleared/set before and reset after each individual test in the annotated class.

@RestoreEnvironmentVariables

@RestoreEnvironmentVariables can be used to restore changes to environment variables made directly in code. While @ClearEnvironmentVariable and @SetEnvironmentVariable set or clear specific variables and values, they don’t allow values to be calculated or parameterized, thus there are times you may want to directly set them in your test code. @RestoreEnvironmentVariables can be placed on test methods or test classes and will completely restore all environment variables to their original state after a test or test class is complete.

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

@ParameterizedTest
@ValueSource(strings = { "foo", "bar" })
@RestoreEnvironmentVariables
void parameterizedTest(String value) {
    setEnvVar("some parameterized property", value);
    setEnvVar("some other dynamic property", "my code calculates somehow");
}
Note

Modifying environment variables of a running JVM requires several lines of code and the Java reflection API. The two @RestoreEnvironmentVariables examples leave out that detail and use a hypothetical setEnvVar() method.

When @RestoreEnvironmentVariables is used on a test class, any environment variable 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 @RestoreEnvironmentVariables.

In the following example, both test methods see the environment variable 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 @RestoreEnvironmentVariables ensures that environment variable 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 @ReadsEnvironmentVariable annotation: This ensures that JUnit does not schedule the class to run during any test known to modify environment variables (see Thread-Safety).

@RestoreEnvironmentVariables
class EnvironmentVarRestoreTest {

    @BeforeAll
    void beforeAll() {
        setEnvVar("A", "A value");
    }

    @BeforeEach
    void beforeEach() {
        setEnvVar("B", "B value");
    }

    @Test
    void isolatedTest1() {
        setEnvVar("C", "C value");
    }

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

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

}

Some other test class, running later:

// A test class that runs later
@ReadsEnvironmentVariable
class SomeOtherTestClass {

    @Test
    void isolatedTest() {
        // Class-level @RestoreEnvironmentVariables restores all changes made in EnvironmentVarRestoreTest
        assertThat(System.getenv("A")).isNull();
        assertThat(System.getenv("B")).isNull();
        assertThat(System.getenv("C")).isNull();
    }

    // Changes to A, B, C have been restored to their values prior to the above test
}

Using @ClearEnvironmentVariable, @SetEnvironmentVariable, and @RestoreEnvironmentVariables together

All three annotations can be combined, which could be used when some environment values 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 environment variables. Basic configuration can be specified using Set and Clear and the image size parameterized:

@ParameterizedTest
@IntRangeSource(from = 0, to = 10000, step = 500)
@RestoreEnvironmentVariables
@SetEnvironmentVariable(key = "DISABLE_CACHE", value = "TRUE")
@ClearEnvironmentVariable(key = "COPYWRITE_OVERLAY_TEXT")
void imageGenerationTest(int imageSize) {
    setEnvVar("IMAGE_SIZE", String.valueOf(imageSize)); // Requires restore

    // Test your image generation utility with the current environment variables
}
Note

Using @RestoreEnvironmentVariables is not necessary to restore environment variables modified via @ClearEnvironmentVariable or @SetEnvironmentVariable - they each automatically restore the referenced variables. 'Restore', is only needed if environment variables are modified in some way other than Clear and Set during a test.

Warnings for Reflective Access

As explained above, this extension uses reflective access to change the otherwise immutable environment variables. On Java 9 to 16, this leads to a warning like the following:

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by org.junitpioneer.jupiter.EnvironmentVariableUtils [...] to field [...]
WARNING: Please consider reporting this to the maintainers of org.junitpioneer.jupiter.EnvironmentVariableUtils
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release

On Java 17 and later, you get this error instead:

java.lang.reflect.InaccessibleObjectException: Unable to make field [...] accessible:
module java.base does not "opens java.lang" to unnamed module [...]

This is because Java 17 is strongly encapsulated and the reflective access to JDK internals is no longer permitted. (For further details, see also JEP 403.)

The best way to prevent these warnings/errors, is to change the code under test, so this extension is no longer needed. However, some tests require environment variables to be cleared, set, or restored. In this case, we recommend using --add-opens to grant JUnit Pioneer access to the aforementiond internals:

# to access java.util.Collections$UnmodifiableMap.m
--add-opens java.base/java.util=$TARGET_MODULE
# to access java.lang.ProcessEnvironment.theEnvironment
--add-opens java.base/java.lang=$TARGET_MODULE

Where $TARGET_MODULE equals ALL-UNNAMED if you place JUnit Pioneer on the class path, or org.junitpioneer if you place JUnit Pioneer on the module path. These command line options need to be added to the JVM that executes the tests:

For instance, if you are using Maven and test on the class path, your Surefire configuration may look like this:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version><!--...--></version>
    <configuration>
        <argLine>
            --add-opens java.base/java.util=ALL-UNNAMED
            --add-opens java.base/java.lang=ALL-UNNAMED
        </argLine>
    </configuration>
</plugin>
Note

Depending on your IDE, these settings may not be picked up. Therefore, you possibly also have to include --add-opens in your test’s run configuration.

Thread-Safety

Since environment variables are global state, reading and writing them during parallel test execution can lead to unpredictable results and flaky tests. The environment variable extension is prepared for that and tests annotated with @ClearEnvironmentVariable, @SetEnvironmentVariable, or @RestoreEnvironmentVariables will never execute in parallel (thanks to resource locks) to guarantee correct test results.

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

  • @ReadsEnvironmentVariable

  • @WritesEnvironmentVariable (though consider using @RestoreEnvironmentVariables instead)

Tests annotated in this way will never execute in parallel with tests annotated with @ClearEnvironmentVariable, @SetEnvironmentVariable, or @RestoreEnvironmentVariables.