1. Overview
Sometimes when writing unit tests, we may need to test code that interacts directly with the System class. Typically in applications such as command-line tools which call System.exit directly or read arguments using System.in.
In this tutorial, we'll take a look at the most common features of a neat external library called System Rules which provides a set of JUnit rules for testing code that uses the System class.
2. Maven Dependencies
First, let’s add the System Rules dependency to our pom.xml:
<dependency>
<groupId>com.github.stefanbirkner</groupId>
<artifactId>system-rules</artifactId>
<version>1.19.0</version>
</dependency>
We'll also add System Lambda dependency which is available from Maven Central as well:
<dependency>
<groupId>com.github.stefanbirkner</groupId>
<artifactId>system-lambda</artifactId>
<version>1.1.0</version>
</dependency>
The reason for adding this last dependency is so we can also show examples where appropriate using JUnit5. As we know, in this version of JUnit, the rules model was replaced by extensions.
3. Working With System Properties
To quickly recap, the Java platform uses a Properties object to provide information about the local system and configuration. We can easily print out the properties:
System.getProperties()
.forEach((key, value) -> System.out.println(key + ": " + value));
As we can see, properties include information such as the current user, the current version of the Java runtime, and file path-name separator:
java.version: 1.8.0_221
file.separator: /
user.home: /Users/baeldung
os.name: Mac OS X
...
We can also set our own system properties by using the System.setProperty method. Care should be taken when working with system properties from our tests, as these properties are JVM-global.
For example, if we set a system property, we should ensure we restore the property to its original value when our test finishes or if a failure occurs. This can sometimes lead to cumbersome setup and tear down code. However, if we neglect to do this, it could lead to unexpected side-effects in our tests.
In the next section, we'll see how we can provide, clean, and make sure we restore system property values after our tests complete in a concise and simple manner.
4. Providing System Properties
Let's imagine that we have a system property log_dir which contains the location of where our logs should be written and our application sets this location when it starts up:
System.setProperty("log_dir", "/tmp/baeldung/logs");
4.1. Provide a Single Property
Now let's consider that from our unit test, we want to provide a different value. We can do this using the ProvideSystemProperty rule:
public class ProvidesSystemPropertyWithRuleUnitTest {
@Rule
public final ProvideSystemProperty providesSystemPropertyRule = new ProvideSystemProperty("log_dir", "test/resources");
@Test
public void givenProvideSystemProperty_whenGetLogDir_thenLogDirIsProvidedSuccessfully() {
assertEquals("log_dir should be provided", "test/resources", System.getProperty("log_dir"));
}
// unit test definition continues
}
Using the ProvideSystemProperty rule, we can set an arbitrary value for a given system property for use from our tests. In this example, we set the log_dir property to our test/resources directory, and from our unit test, simply assert that the test property value has been provided successfully.
If we then print out the value of the log_dir property when our test class completes:
@AfterClass
public static void tearDownAfterClass() throws Exception {
System.out.println(System.getProperty("log_dir"));
}
We can see the value of the property has been restored to its original value:
/tmp/baeldung/logs
4.2. Providing Multiple Properties
If we need to provide multiple properties, we can use the and method to chain as many property values together as we require for our test:
@Rule
public final ProvideSystemProperty providesSystemPropertyRule =
new ProvideSystemProperty("log_dir", "test/resources").and("another_property", "another_value")
4.3. Providing Properties From a File
Likewise, we also have the possibility of providing properties from a file or classpath resource using the ProvideSystemProperty rule:
@Rule
public final ProvideSystemProperty providesSystemPropertyFromFileRule =
ProvideSystemProperty.fromResource("/test.properties");
@Test
public void givenProvideSystemPropertyFromFile_whenGetName_thenNameIsProvidedSuccessfully() {
assertEquals("name should be provided", "baeldung", System.getProperty("name"));
assertEquals("version should be provided", "1.0", System.getProperty("version"));
}
In the above example, we assume that we have a test.properties file on the classpath:
name=baeldung
version=1.0
4.4. Providing Properties With JUnit5 and Lambdas
As we previously mentioned, we could also use the System Lamda version of the library to implement tests compatible with JUnit5.
Let's see how to implement our test using this version of the library:
@BeforeAll
static void setUpBeforeClass() throws Exception {
System.setProperty("log_dir", "/tmp/baeldung/logs");
}
@Test
void givenSetSystemProperty_whenGetLogDir_thenLogDirIsProvidedSuccessfully() throws Exception {
restoreSystemProperties(() -> {
System.setProperty("log_dir", "test/resources");
assertEquals("log_dir should be provided", "test/resources", System.getProperty("log_dir"));
});
assertEquals("log_dir should be provided", "/tmp/baeldung/logs", System.getProperty("log_dir"));
}
In this version, we can use the restoreSystemProperties method to execute a given statement. Inside this statement, we can set up and provide the values we require for our system properties. As we can see after this method finishes execution, the value of log_dir is the same as before /tmp/baeldung/logs.
Unfortunately, there is no built-in support for providing properties from files using the restoreSystemProperties method.
5. Clearing System Properties
Sometimes we might want to clear a set of system properties when our test starts and restore their original values when the test finishes irrespective of whether it passes or fails.
We can use the ClearSystemProperties rule for this purpose:
@Rule
public final ClearSystemProperties userNameIsClearedRule = new ClearSystemProperties("user.name");
@Test
public void givenClearUsernameProperty_whenGetUserName_thenNull() {
assertNull(System.getProperty("user.name"));
}
The system property user.name is one of the predefined system properties, which contains the user account name. As expected in the above unit test, we clear this property and check it is empty from our test.
Conveniently, we can also pass multiple property names to the ClearSystemProperties constructor.
6. Mocking System.in
From time to time, we might create interactive command-line applications that read from System.in.
For this section, we'll use a very simple example which reads a first name and surname from the standard input and concatenates them together:
private String getFullname() {
try (Scanner scanner = new Scanner(System.in)) {
String firstName = scanner.next();
String surname = scanner.next();
return String.join(" ", firstName, surname);
}
}
System Rules contains the TextFromStandardInputStream rule which we can use to specify the lines that should be provided when calling System.in:
@Rule
public final TextFromStandardInputStream systemInMock = emptyStandardInputStream();
@Test
public void givenTwoNames_whenSystemInMock_thenNamesJoinedTogether() {
systemInMock.provideLines("Jonathan", "Cook");
assertEquals("Names should be concatenated", "Jonathan Cook", getFullname());
}
We accomplish this by using the providesLines method, which takes a varargs parameter to enable specifying more than one value.
In this example, we provide two values before calling the getFullname method, where System.in is referenced. Our two provided line values will be returned each time we call scanner.next().
Let's take a look at how we can achieve the same in a JUnit 5 version of the test using System Lambda:
@Test
void givenTwoNames_whenSystemInMock_thenNamesJoinedTogether() throws Exception {
withTextFromSystemIn("Jonathan", "Cook").execute(() -> {
assertEquals("Names should be concatenated", "Jonathan Cook", getFullname());
});
}
In this variation, we use the similarly named withTextFromSystemIn method, which lets us specify the provided System.in values.
It is important to mention in both cases that after the test finishes, the original value of System.in will be restored.
7. Testing System.out and System.err
In a previous tutorial, we saw how to use System Rules to unit test System.out.println().
Conveniently, we can apply an almost identical approach for testing code which interacts with the standard error stream. This time we use the SystemErrRule:
@Rule
public final SystemErrRule systemErrRule = new SystemErrRule().enableLog();
@Test
public void givenSystemErrRule_whenInvokePrintln_thenLogSuccess() {
printError("An Error occurred Baeldung Readers!!");
Assert.assertEquals("An Error occurred Baeldung Readers!!",
systemErrRule.getLog().trim());
}
private void printError(String output) {
System.err.println(output);
}
Nice! Using the SystemErrRule, we can intercept the writes to System.err. First, we start logging everything written to System.err by calling the enableLog method on our rule. Then we simply call getLog to get the text written to System.err since we called enableLog.
Now, let's implement the JUnit5 version of our test:
@Test
void givenTapSystemErr_whenInvokePrintln_thenOutputIsReturnedSuccessfully() throws Exception {
String text = tapSystemErr(() -> {
printError("An error occurred Baeldung Readers!!");
});
Assert.assertEquals("An error occurred Baeldung Readers!!", text.trim());
}
In this version, we make use of the tapSystemErr method, which executes the statement and lets us capture the content passed to System.err.
8. Handling System.exit
Command-line applications typically terminate by calling System.exit. If we want to test such an application, it is likely that our test will terminate abnormally before it finishes when it encounters the code which calls System.exit.
Thankfully, System Rules provides a neat solution to handle this using the ExpectedSystemExit rule:
@Rule
public final ExpectedSystemExit exitRule = ExpectedSystemExit.none();
@Test
public void givenSystemExitRule_whenAppCallsSystemExit_thenExitRuleWorkssAsExpected() {
exitRule.expectSystemExitWithStatus(1);
exit();
}
private void exit() {
System.exit(1);
}
Using the ExpectedSystemExit rule allows us to specify from our test the expected System.exit() call. In this simple example, we also check the expected status code using the expectSystemExitWithStatus method.
We can achieve something similar in our JUnit 5 version using the catchSystemExit method:
@Test
void givenCatchSystemExit_whenAppCallsSystemExit_thenStatusIsReturnedSuccessfully() throws Exception {
int statusCode = catchSystemExit(() -> {
exit();
});
assertEquals("status code should be 1:", 1, statusCode);
}
9. Conclusion
To summarize, in this tutorial, we've explored the System Rules library in detail.
First, we started by explaining how to test code that uses system properties. Then we looked at how to test the standard output and standard input. Finally, we looked at how to handle code which calls System.exit from our tests.
The System Rules library also provides support for providing environment variables and special security mangers from our tests. Be sure to check out the full documentation for details.
As always, the full source code of the article is available over on GitHub.
The post Guide to the System Rules Library first appeared on Baeldung.