1. Overview

It can be hard to test our software when it depends on system resources like environment variables, system properties, or uses process-level operations like System.exit.

Java doesn’t provide a direct method for setting environment variables, and we run the risk of the values set in one test affecting the execution of another. Similarly, we may find ourselves avoiding writing JUnit tests for code that might perform a System.exit as there’s a chance it would abort the tests.

The System Rules and System Lambda Libraries were early solutions to these problems. In this tutorial, we’ll look at a fork of System Lambda called System Stubs, which provides a JUnit 5 alternative.

2. Why System Stubs?

2.1. System Lambda Is Not a JUnit Plugin

The original System Rules library was only usable with JUnit 4. It could still be used with JUnit Vintage under JUnit 5, but that required the continued creation of JUnit 4 tests. The creators of the library produced a test framework agnostic version called System Lambda, which was intended for use inside each test method:

@Test
void aSingleSystemLambda() throws Exception {
    restoreSystemProperties(() -> {
        System.setProperty("log_dir", "test/resources");
        assertEquals("test/resources", System.getProperty("log_dir"));
    });

    // more test code here
}

The test code is expressed as a lambda, passed to a method that sets up the necessary stubbing. The cleanup happens just before control is returned to the rest of the test method.

Though this works well in some cases, the approach has a few disadvantages.

2.2. Avoiding Extra Code

The benefit of the System Lambda approach is that there are some common recipes inside its factory class for performing specific types of tests. However, this leads to some code bloat when we want to use it across many test cases.

Firstly, even if the test code itself doesn’t throw a checked exception, the wrapper method does, so all methods gain a throws Exception. Secondly, setting up the same rule across multiple tests requires code duplication. Each test needs to perform the same configuration independently.

However, the most cumbersome aspect of this approach comes when we try to set up more than one tool at a time. Let’s say we want to set some environment variables and system properties. We end up needing two levels of nesting before our test code starts:

@Test
void multipleSystemLambdas() throws Exception {
    restoreSystemProperties(() -> {
        withEnvironmentVariable("URL", "https://www.baeldung.com")
            .execute(() -> {
                System.setProperty("log_dir", "test/resources");
                assertEquals("test/resources", System.getProperty("log_dir"));
                assertEquals("https://www.baeldung.com", System.getenv("URL"));
            });
    });
}

This is where a JUnit plugin or extension can help us cut down the amount of code we need in our tests.

2.3. Using Less Boilerplate

We should expect to be able to write our tests with a minimum of boilerplate:

@SystemStub
private EnvironmentVariables environmentVariables = ...;

@SystemStub
private SystemProperties restoreSystemProperties;

@Test
void multipleSystemStubs() {
    System.setProperty("log_dir", "test/resources");
    assertEquals("test/resources", System.getProperty("log_dir"));
    assertEquals("https://www.baeldung.com", System.getenv("ADDRESS"));
}

This approach is provided by the SystemStubs JUnit 5 extension and allows our tests to be composed with less code.

2.4. Test Lifecycle Hooks

When the only available tool is the execute-around pattern, it’s impossible to hook in the stubbing behavior to all parts of the test lifecycle. This is particularly challenging when trying to combine it with other JUnit extensions, like @SpringBootTest.

If we wanted to set up some environment variables around a Spring Boot test, then there is no way we could reasonably embed that whole test eco-system inside a single test method. We would need a way to activate the test set-up around a test suite.

This was never going to be possible with the methodology employed by System Lambda and was one of the main reasons to create System Stubs.

2.5. Encourage Dynamic Properties

Other frameworks for setting system properties, such as JUnit Pioneer, emphasize configurations known at compile time. In modern tests, where we may be using Testcontainers or Wiremock, we need to set up our system properties based on random runtime settings after those tools startup. This works best with a test library that can be used across the whole test lifecycle.

2.6. More Configurability

It’s beneficial having ready-made test recipes, like catchSystemExit, which wrap around test code to do a single job. However, this relies on the test library developers to provide each variation of configuration option we might need.

Configuration by composition is more flexible and is a large part of the new System Stubs implementation.

However, System Stubs supports the original test constructs from System Lambda for backward compatibility. Additionally, it provides a new JUnit 5 extension, a set of JUnit 4 rules, and many more configuration options. Though based on the original code, it has been heavily refactored and modularised to provide a richer set of features.

Let’s learn more about it.

2.7. JDK Compatibility

After Java 16, it became harder to modify Java environment variables, owing to tighter controls over reflective access. Since version 2, System Stubs modifies environment variables without using reflection, making it easier to use with newer JDK versions..

3. Getting Started

3.1. Dependencies

The JUnit 5 extension requires a reasonably up to date version of JUnit 5:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.10.2</version>
    <scope>test</scope>
</dependency>

Let’s add all the System Stubs library dependencies to our pom.xml:

<!-- for testing with only lambda pattern -->
<dependency>
    <groupId>uk.org.webcompere</groupId>
    <artifactId>system-stubs-core</artifactId>
    <version>2.1.6</version>
    <scope>test</scope>
</dependency>

<!-- for JUnit 4 -->
<dependency>
    <groupId>uk.org.webcompere</groupId>
    <artifactId>system-stubs-junit4</artifactId>
    <version>2.1.6</version>
    <scope>test</scope>
</dependency>

<!-- for JUnit 5 -->
<dependency>
    <groupId>uk.org.webcompere</groupId>
    <artifactId>system-stubs-jupiter</artifactId>
    <version>2.1.6</version>
    <scope>test</scope>
</dependency>

We should note that we only need to import as many of these as we need for the test framework we’re using. Indeed, both the latter two transitively include the core dependency.

Now let’s write our first test.

3.2. JUnit 4 Environment Variables

We can control environment variables by declaring a JUnit 4 @Rule annotated field in our test class of type EnvironmentVariablesRule. This will be activated by JUnit 4 when our tests run and will allow us to set environment variables inside the test:

@Rule
public EnvironmentVariablesRule environmentVariablesRule = new EnvironmentVariablesRule();

@Test
public void givenEnvironmentCanBeModified_whenSetEnvironment_thenItIsSet() {
    environmentVariablesRule.set("ENV", "value1");

    assertThat(System.getenv("ENV")).isEqualTo("value1");
}

In practice, we may prefer to set the environment variable values in a @Before method so that the set-up can be shared across all tests:

@Before
public void before() {
    environmentVariablesRule.set("ENV", "value1")
      .set("ENV2", "value2");
}

Here we should note using the fluent set method, which makes setting multiple values easy through method chaining.

We can also use the constructor of the EnvironmentVariablesRule object to provide values on construction:

@Rule
public EnvironmentVariablesRule environmentVariablesRule =
  new EnvironmentVariablesRule("ENV", "value1",
    "ENV2", "value2");

There are several overloads of the constructor, allowing variables to be provided in different forms. The one in the above example allows any number of name-value pairs to be provided using varargs.

Each of the System Stubs JUnit 4 rules is a subclass of one of the core stubbing objects. They can also be used across the lifecycle of a whole test class with the @ClassRule annotation on a static field, which will cause them to be activated before the first test, and then cleaned up just after the last.

3.3. JUnit 5 Environment Variables

Before we use System Stubs objects inside a JUnit 5 test, we must add the extension to our test class:

@ExtendWith(SystemStubsExtension.class)
class EnvironmentVariablesJUnit5 {
    // tests
}

Then we can create a field in the test class for JUnit 5 to manage for us. We annotate this with @SystemStub so that the extension knows to activate it:

@SystemStub
private EnvironmentVariables environmentVariables;

The extension will only manage objects marked with @SystemStub, which allows us to use other System Stubs objects in the test manually if we prefer.

Here, we haven’t provided any construction of the stub object. The extension constructs one for us, in the same way as the Mockito extension constructs mocks.

We can now use the object to help us set environment variables inside one of our tests:

@Test
void givenEnvironmentCanBeModified_whenSetEnvironment_thenItIsSet() {
    environmentVariables.set("ENV", "value1");

    assertThat(System.getenv("ENV")).isEqualTo("value1");
}

If we wanted to provide the environment variables that apply to all tests from outside of the test method, we can do that inside a @BeforeEach method or can use the constructor of EnvironmentVariables to set our values:

@SystemStub
private EnvironmentVariables environmentVariables =
  new EnvironmentVariables("ENV", "value1");

As with EnvironmentVariablesRule there are several overloads of the constructor, allowing us many ways to set the desired variables. We can also use the set method fluently to set values if we prefer:

@SystemStub
private EnvironmentVariables environmentVariables =
  new EnvironmentVariables()
    .set("ENV", "value1")
    .set("ENV2", "value2");

We can also make our fields static for them to be managed as part of the @BeforeAll/@AfterAll lifecycle.

3.4. JUnit 5 Parameter Injection

While placing the stub objects in fields is useful when using them for all of our tests, we may prefer to use them only for selected ones. This can be achieved by JUnit 5 parameter injection:

@Test
void givenEnvironmentCanBeModified(EnvironmentVariables environmentVariables) {
    environmentVariables.set("ENV", "value1");

    assertThat(System.getenv("ENV")).isEqualTo("value1");
}

In this case, the EnvironmentVariables object was constructed for us with its default constructor, allowing us to use it within a single test. The object has also been activated so that it is operating on the runtime environment. It will be tidied up when the test is finished.

All of the System Stubs objects have a default constructor and the ability to be reconfigured while running. We can inject as many as we need into our tests.

3.5. Execute-Around Environment Variables

The original System Lambda facade methods for creating stubs are also available via the SystemStubs class. Internally they are implemented by creating instances of the stubbing objects. Sometimes the object returned from the recipe is a stub object for further configuration and use:

withEnvironmentVariable("ENV3", "val")
    .execute(() -> {
        assertThat(System.getenv("ENV3")).isEqualTo("val");
    });

Behind the scenes, withEnvironmentVariable is doing the equivalent of:

return new EnvironmentVariables().set("ENV3", "val");

The execute method is common to all SystemStub objects. It sets up the stubbing defined by the object, then executes the lambda passed in. Afterward, it tidies up and returns control to the surrounding test.

If the test code returns a value, then that value can be returned by execute:

String extracted = new EnvironmentVariables("PROXY", "none")
  .execute(() -> System.getenv("PROXY"));

assertThat(extracted).isEqualTo("none");

This can be useful when the code we are testing needs to have access to environment settings to construct something. It’s commonly used when testing things like AWS Lambda handlers, which are often configured through environment variables.

The advantage of this pattern for occasional tests is that we have to set up the stubbing explicitly, only where needed. Therefore it can be more precise and visible. However, it does not allow us to share the setup between tests and can be more long-winded.

3.6. Multiple System Stubs

We have already seen how the JUnit 4 and JUnit 5 plugins construct and activate stubbing objects for us. If there are multiple stubs, they are set up and torn down appropriately by the framework code.

However, when we construct stubbing objects for the execute-around pattern, we need our test code to run inside them all.

This can be achieved using the with/execute methods. These work by creating a composite from multiple stubbing objects used with a single execute:

with(new EnvironmentVariables("FOO", "bar"), new SystemProperties("prop", "val"))
  .execute(() -> {
      assertThat(System.getenv("FOO")).isEqualTo("bar");
      assertThat(System.getProperty("prop")).isEqualTo("val");
  });

Now we have seen the general form of using the System Stubs objects, both with and without JUnit framework support, let’s look at the rest of the library’s capabilities.

4. System Properties

We can call System.setProperty at any time in Java. However, this runs the risk of leaking the settings out of one test into another. The primary aim of SystemProperties stubbing is to restore the system properties to their original settings after the test is complete. However, it’s also useful for common setup code to define which system properties should be used before the test starts.

4.1. JUnit 4 System Properties

By adding the rule to the JUnit 4 test class, we can insulate each test from any System.setProperty calls made in other test methods. We can also provide some upfront properties via the constructor:

@Rule
public SystemPropertiesRule systemProperties =
  new SystemPropertiesRule("db.connection", "false");

With this object, we can also set some additional properties in the JUnit @Before method:

@Before
public void before() {
    systemProperties.set("before.prop", "before");
}

We can also use the set method in the body of a test or use System.setProperty if we wish. We must only use set in creating the SystemPropertiesRule, or in the @Before method, as it stores the setting in the rule, ready for applying later.

4.2. JUnit 5 System Properties

We have two main use cases for using the SystemProperties object. We may wish to reset the system properties after each test case, or we may wish to prepare some common system properties in a central place for each test case to use.

Restoring system properties requires us to add both the JUnit 5 extension and a SystemProperties field to our test class:

@ExtendWith(SystemStubsExtension.class)
class RestoreSystemProperties {
    @SystemStub
    private SystemProperties systemProperties;

}

Now, each test will have any system properties it changes cleaned up afterward.

We can also do this for selected tests by parameter injection:

@Test
void willRestorePropertiesAfter(SystemProperties systemProperties) {

}

If we want the test to have properties set in it, then we can either assign those properties in the construction of our SystemProperties object or use a @BeforeEach method:

@ExtendWith(SystemStubsExtension.class)
class SetSomeSystemProperties {
    @SystemStub
    private SystemProperties systemProperties;

    @BeforeEach
    void before() {
        systemProperties.set("beforeProperty", "before");
    }
}

Again, let’s note that the JUnit 5 test needs to be annotated with @ExtendWith(SystemStubsExtension.class). The extension will create the System Stubs object if we do not provide a new statement in the initializer list.

4.3. System Properties with Execute Around

The SystemStubs class provides a restoreSystemProperties method to allow us to run test code with properties restored:

restoreSystemProperties(() -> {
    // test code
    System.setProperty("unrestored", "true");
});

assertThat(System.getProperty("unrestored")).isNull();

This takes a lambda that returns nothing. If we wish to use a common set-up function to create properties, get a return value from the test method, or combine SystemProperties with other stubs via with/execute, then we can create the object explicitly:

String result = new SystemProperties()
  .execute(() -> {
      System.setProperty("unrestored", "true");
      return "it works";
  });

assertThat(result).isEqualTo("it works");
assertThat(System.getProperty("unrestored")).isNull();

4.4. Properties in Files

Both the SystemProperties and EnvironmentVariables objects can be constructed from a Map. This allows Java’s Properties object to be provided as the source of either system properties or environment variables.

There are helper methods inside the PropertySource class for loading up Java properties from files or resources. These properties files are name/value pairs:

name=baeldung
version=1.0

We can load from the resource test.properties by using the fromResource function:

SystemProperties systemProperties =
  new SystemProperties(PropertySource.fromResource("test.properties"));

There are similar convenience methods in PropertySource for other sources, such as fromFile or fromInputStream.

5. System.out and System.err

When our application writes to System.out, it can be hard to test. This is sometimes solved by using an interface as the target of output and mocking that at test time:

interface LogOutput {
   void write(String line);
}

class Component {
    private LogOutput log;

    public void method() {
        log.write("Some output");
    }
}

Techniques like this work well with Mockito mocks but are not necessary if we can just trap System.out itself.

5.1. JUnit 4 SystemOutRule and SystemErrRule

To trap output to System.out in a JUnit 4 test, we add the SystemOutRule:

@Rule
public SystemOutRule systemOutRule = new SystemOutRule();

After that, any output to System.out can be read within the test:

System.out.println("line1");
System.out.println("line2");

assertThat(systemOutRule.getLines())
  .containsExactly("line1", "line2");

We have a choice of formats for the text. The above example uses the Stream provided by getLines. We may also choose to get the whole block of text:

assertThat(systemOutRule.getText())
  .startsWith("line1");

However, we should note that this text will have newline characters that vary between platforms. We can replace newlines with \n on every platform by using the normalized form:

assertThat(systemOutRule.getLinesNormalized())
  .isEqualTo("line1\nline2\n");

The SystemErrRule works in the same way for System.err as its System.out counterpart:

@Rule
public SystemErrRule systemErrRule = new SystemErrRule();

@Test
public void whenCodeWritesToSystemErr_itCanBeRead() {
    System.err.println("line1");
    System.err.println("line2");

    assertThat(systemErrRule.getLines())
      .containsExactly("line1", "line2");
}

There is also a SystemErrAndOutRule class, which taps both System.out and System.err simultaneously into a single buffer.

5.2. JUnit 5 Example

As with the other System Stubs objects, we only need to declare a field or parameter of type SystemOut or SystemErr. This will provide us with a capture of the output:

@SystemStub
private SystemOut systemOut;

@SystemStub
private SystemErr systemErr;

@Test
void whenWriteToOutput_thenItCanBeAsserted() {
    System.out.println("to out");
    System.err.println("to err");

    assertThat(systemOut.getLines()).containsExactly("to out");
    assertThat(systemErr.getLines()).containsExactly("to err");
}

We can also use the SystemErrAndOut class to direct both sets of output into the same buffer.

5.3. Execute-Around Example

The SystemStubs facade provides some functions for tapping the output and returning it as a String:

@Test
void givenTapOutput_thenGetOutput() throws Exception {
    String output = tapSystemOutNormalized(() -> {
        System.out.println("a");
        System.out.println("b");
    });

    assertThat(output).isEqualTo("a\nb\n");
}

We should note that these methods do not provide as rich an interface as the raw objects themselves. The capture of output can’t easily be combined with other stubbing, such as setting environment variables.

However, the SystemOut, SystemErr, and SystemErrAndOut objects can be used directly. For example, we could combine them with some SystemProperties:

SystemOut systemOut = new SystemOut();
SystemProperties systemProperties = new SystemProperties("a", "!");
with(systemOut, systemProperties)
  .execute(()  -> {
    System.out.println("a: " + System.getProperty("a"));
});

assertThat(systemOut.getLines()).containsExactly("a: !");

5.4. Muting

Sometimes our aim is not to capture output but to keep it from cluttering our test run logs. We can achieve this using the muteSystemOut or muteSystemErr functions:

muteSystemOut(() -> {
    System.out.println("nothing is output");
});

We can achieve the same thing across all tests via the JUnit 4 SystemOutRule:

@Rule
public SystemOutRule systemOutRule = new SystemOutRule(new NoopStream());

In JUnit 5, we can use the same technique:

@SystemStub
private SystemOut systemOut = new SystemOut(new NoopStream());

5.5. Customization

As we have seen, there are several variations for intercepting output. They all share a common base class in the library. For convenience, several helper methods and types, like SystemErrAndOut, help do common things. However, the library itself is easily customized.

We could provide our own target for capturing the output as an implementation of Output. We’ve already seen the Output class TapStream in use in the first examples. NoopStream is used for muting. We also have DisallowWriteStream that throws an error if something writes to it:

// throws an exception:
new SystemOut(new DisallowWriteStream())
  .execute(() -> System.out.println("boo"));

6. Mocking System In

We may have an application that reads input on stdin. Testing this could involve extracting the algorithm into a function that reads from any InputStream and then feeding it with a pre-preprepared input stream. In general, modular code is better, so this is a good pattern.

However, if we only test the core functions, we lose test coverage on the code which provides System.in as the source.

In any case, it can be inconvenient to construct our own streams. Luckily, System Stubs has solutions for all of these.

6.1. Test Input Streams

System Stubs provides a family of AltInputStream classes as alternative inputs for any code that reads from an InputStream:

LinesAltStream testInput = new LinesAltStream("line1", "line2");

Scanner scanner = new Scanner(testInput);
assertThat(scanner.nextLine()).isEqualTo("line1");

In this example, we’ve used an array of strings to construct LinesAltStream, but *we could have supplied the input from a Stream, allowing this to be used with any source of text data* without necessarily loading it all into memory at once.

6.2. JUnit 4 Example

We can provide lines for input in a JUnit 4 test using the SystemInRule:

@Rule
public SystemInRule systemInRule =
  new SystemInRule("line1", "line2", "line3");

Then, the test code can read this input from System.in:

@Test
public void givenInput_canReadFirstLine() {
    assertThat(new Scanner(System.in).nextLine())
      .isEqualTo("line1");
}

6.3. JUnit 5 Example

For JUnit 5 tests, we create a SystemIn field:

@SystemStub
private SystemIn systemIn = new SystemIn("line1", "line2", "line3");

Then our tests will run with System.in providing these lines as input.

6.4. Execute-Around Example

The SystemStubs facade provides withTextFromSystemIn as a factory method that creates a SystemIn object for use with its execute method:

withTextFromSystemIn("line1", "line2", "line3")
  .execute(() -> {
      assertThat(new Scanner(System.in).nextLine())
        .isEqualTo("line1");
  });

6.5. Customization

More features can be added to the SystemIn object either on construction or while it is running within a test.

We can call andExceptionThrownOnInputEnd, which causes reading from System.in to throw an exception when it runs out of text. This can simulate an interrupted read from a file.

We can also set the input stream to come from any InputStream, like FileInputStream, by using setInputStream. We also have LinesAltStream and TextAltStream, which operate on the input text.

7. Mocking System.Exit

As mentioned previously, if our code can call System.exit, it can make for dangerous and hard to debug test faults. One of our aims in stubbing System.exit is to make an accidental call into a traceable error. Another motivation is to test intentional exits from the software.

7.1. JUnit 4 Example

Let’s add the SystemExitRule to a test class as a safety measure to prevent any System.exit from stopping the JVM:

@Rule
public SystemExitRule systemExitRule = new SystemExitRule();

However, we may also wish to see if the right exit code was used. For that, we need to assert that the code throws the AbortExecutionException, which is the System Stubs signal that System.exit was called.

@Test
public void whenExit_thenExitCodeIsAvailable() {
    assertThatThrownBy(() -> {
        System.exit(123);
    }).isInstanceOf(AbortExecutionException.class);

    assertThat(systemExitRule.getExitCode()).isEqualTo(123);
}

In this example, we’ve used assertThatThrownBy from AssertJ to catch and check the exception signaling exit occurred. Then we looked at getExitCode from the SystemExitRule to assert the exit code.

7.2. JUnit 5 Example

For JUnit 5 tests, we declare the @SystemStub field:

@SystemStub
private SystemExit systemExit;

Then we use the SystemExit class in the same way as SystemExitRule in JUnit 4. Given that the SystemExitRule class is a subclass of SystemExit, they have the same interface.

7.3. Execute-Around Example

The SystemStubs class provides catchSystemExit, which internally uses SystemExit‘s execute function:

int exitCode = catchSystemExit(() -> {
    System.exit(123);
});
assertThat(exitCode).isEqualTo(123);

Compared with the JUnit plugin examples, this code does not throw an exception to indicate a system exit. Instead, it catches the error and records the exit code. With the facade method, it returns the exit code.

When we use the execute method directly, the exit is caught, and the exit code is set inside the SystemExit object. We can then call getExitCode to get the exit code, or null if there was none.

8. Custom Test Resources in JUnit 5

JUnit 4 already provides a simple structure for creating test rules like the ones used in System Stubs. If we want to make a new test rule for some resource, with a setup and teardown, we can subclass ExternalResource and provide overrides of the before and after methods.

JUnit 5 has a more complex pattern for resource management. For simple use cases, it’s possible to use the System Stubs library as a starting point. The SystemStubsExtension operates on anything that satisfies the TestResource interface.

8.1. Creating a TestResource

We can create a subclass of TestResource and then use our custom objects in the same way we use System Stubs ones. We should note that we need to provide a default constructor if we want to use the automatic creation of fields and parameters.

Let’s say we wanted to open a connection to a database for some tests and close it afterward:

public class FakeDatabaseTestResource implements TestResource {
    // let's pretend this is a database connection
    private String databaseConnection = "closed";

    @Override
    public void setup() throws Exception {
        databaseConnection = "open";
    }

    @Override
    public void teardown() throws Exception {
        databaseConnection = "closed";
    }

    public String getDatabaseConnection() {
        return databaseConnection;
    }
}

We’re using the databaseConnection string as an illustration of a resource like a database connection. We modify the state of the resource in the setup and teardown methods.

8.2. Execute-Around Is Built-In

Now let’s try using this with the execute-around pattern:

FakeDatabaseTestResource fake = new FakeDatabaseTestResource();
assertThat(fake.getDatabaseConnection()).isEqualTo("closed");

fake.execute(() -> {
    assertThat(fake.getDatabaseConnection()).isEqualTo("open");
});

As we can see, the TestResource interface gave it the execute-around capabilities of the other objects.

8.3. Custom TestResource in JUnit 5 Test

We can also use this inside a JUnit 5 test:

@ExtendWith(SystemStubsExtension.class)
class FakeDatabaseJUnit5UnitTest {

    @Test
    void useFakeDatabase(FakeDatabaseTestResource fakeDatabase) {
        assertThat(fakeDatabase.getDatabaseConnection()).isEqualTo("open");
    }
}

So, it is easy to create additional test objects that follow the System Stubs design.

9. Environment and Property Overrides for JUnit 5 Spring Tests

Setting environment variables for Spring tests can be difficult. We might compose a custom rule for integration testing to set some system properties for Spring to pick up.

We may also use an ApplicationContextInitializer class to plug into our Spring Context, providing extra properties for the test.

As many Spring applications are controlled by system property or environment variable overrides, it may be easier to use System Stubs to set these in an outer test, with the Spring test running as an inner class.

There’s a full example provided in the System Stubs documentation. We start by creating an outer class:

@ExtendWith(SystemStubsExtension.class)
public class SpringAppWithDynamicPropertiesTest {

    // sets the environment before Spring even starts
    @SystemStub
    private static EnvironmentVariables environmentVariables;
}

In this instance, the @SystemStub field is static and is initialized in the @BeforeAll method:

@BeforeAll
static void beforeAll() {
     String baseUrl = ...;

     environmentVariables.set("SERVER_URL", baseUrl);
}

This point in the test lifecycle allows some global resources to be created and applied to the running environment before the Spring test runs.

Then, we can put the Spring test into a @Nested class. This causes it to be run only when the parent class is set up:

@Nested
@SpringBootTest(classes = {RestApi.class, App.class},
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class InnerSpringTest {
    @LocalServerPort
    private int serverPort;

    // Test methods
}

The Spring context is created against the state of the environment set by the @SystemStub objects in the outer class.

This technique also allows us to control the configuration of any other libraries that depend on the state of system properties or environment variables that may be running behind Spring Beans.

This can allow us to hook into the test lifecycle to modify things like proxy settings or HTTP connection pool parameters before a Spring test runs.

10. Conclusion

In this article, we’ve looked at the importance of being able to mock system resources and how System Stubs allows for complex configurations of stubbing with a minimum of code repetition through its JUnit 4 and JUnit 5 plugins.

We saw how to provide and isolate environment variables and system properties in our tests. Then we looked at capturing the output and controlling the input on the standard streams. We also looked at capturing and asserting calls to System.exit.

Finally, we looked at how to create custom test resources and how to use System Stubs with Spring.

As always, the full source code of the examples is available over on GitHub.