Most solutions will
- terminate the test (method, not the entire run) the moment
System.exit()
is called
- ignore an already installed
SecurityManager
- Sometimes be quite specific to a test framework
- restrict to be used at max once per test case
Thus, most solutions are not suited for situations where:
- Verification of side-effects are to be performed after the call to
System.exit()
- An existing security manager is part of the testing.
- A different test framework is used.
- You want to have multiple verifications in a single test case. This may be strictly not recommended, but can be very convenient at times, especially in combination with
assertAll()
, for example.
I was not happy with the restrictions imposed by the existing solutions presented in the other answers, and thus came up with something on my own.
The following class provides a method assertExits(int expectedStatus, Executable executable)
which asserts that System.exit()
is called with a specified status
value, and the test can continue after it. It works the same way as JUnit 5 assertThrows
. It also respects an existing security manager.
There is one remaining problem: When the code under test installs a new security manager which completely replaces the security manager set by the test. All other SecurityManager
-based solutions known to me suffer the same problem.
import java.security.Permission;
import static java.lang.System.getSecurityManager;
import static java.lang.System.setSecurityManager;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
public enum ExitAssertions {
;
public static <E extends Throwable> void assertExits(final int expectedStatus, final ThrowingExecutable<E> executable) throws E {
final SecurityManager originalSecurityManager = getSecurityManager();
setSecurityManager(new SecurityManager() {
@Override
public void checkPermission(final Permission perm) {
if (originalSecurityManager != null)
originalSecurityManager.checkPermission(perm);
}
@Override
public void checkPermission(final Permission perm, final Object context) {
if (originalSecurityManager != null)
originalSecurityManager.checkPermission(perm, context);
}
@Override
public void checkExit(final int status) {
super.checkExit(status);
throw new ExitException(status);
}
});
try {
executable.run();
fail("Expected System.exit(" + expectedStatus + ") to be called, but it wasn't called.");
} catch (final ExitException e) {
assertEquals(expectedStatus, e.status, "Wrong System.exit() status.");
} finally {
setSecurityManager(originalSecurityManager);
}
}
public interface ThrowingExecutable<E extends Throwable> {
void run() throws E;
}
private static class ExitException extends SecurityException {
final int status;
private ExitException(final int status) {
this.status = status;
}
}
}
You can use the class like this:
@Test
void example() {
assertExits(0, () -> System.exit(0)); // succeeds
assertExits(1, () -> System.exit(1)); // succeeds
assertExits(2, () -> System.exit(1)); // fails
}
The code can easily be ported to JUnit 4, TestNG, or any other framework, if necessary. The only framework-specific element is failing the test. This can easily be changed to something framework-independent (other than a Junit 4 Rule
There is room for improvement, for example, overloading assertExits()
with customizable messages.
System.exit()
is bad - your program should fail fast. Throwing an exception merely prolongs an application in an invalid state in situations where a developer wants to exit and will give spurious errors. – Haug