Using JUnit for testing System.exit() calls from application code can be tricky. This is because System.exit()
terminates the JVM running it. If you’re running JUnit, this is the JUnit runner. If JUnit invokes System.exit()
in application code it will end your test without deciding a success / fail status and will also terminate the test run.
When I run a JUnit test that invokes System.exit()
, I get a result like this:
However, if we properly handle System.exit()
from the test code we can continue. We can even test if System.exit()
was called if necessary.
Smells
First, the obvious warning. Just because you can do this, it doesn’t mean you should. Code littered with calls to System.exit()
calls can be considered a smell. A better strategy is usually to handle failure by throwing Exceptions. It’s easier to test if application code throws an Exception than it is to test if it calls System.exit()
. You can translate Exceptions to system error codes in one place only, removing duplication and simplifying changes later. Also, it’s an interaction with the external system so likely does not logically belong in regular application code.
That said, if you really need to test System.exit()
calls from your application code, read on.
Disallowing System.exit()
One way to prevent System.exit()
from killing our test runner is to disallow it from the SecurityManager. The SecurityManager
is a legacy (pre JDK 17) mechanism to disallow certain actions. The checkExit() method controls access to System.exit()
. If we want to disallow System.exit()
, simply have it throw a SecurityException:
class DisallowExitSecurityManager extends SecurityManager {
@Override
public void checkExit(int status) {
throw new SystemExitException(status);
}
}
When an application (or a test) runs with this SecurityManager
, calls to System.exit()
will result in a SecurityException
and not termination of the JVM.
Testing System.exit() from JUnit 4
We can use our custom SecurityManager
from JUnit 4 tests by creating a JUnit 4 Rule that registers our SecurityManager
before it runs the test.
public class SystemExitRule extends TestWatcher {
private SecurityManager originalSecurityManager;
@Override
protected void starting(Description description) {
originalSecurityManager = System.getSecurityManager();
DisallowExitSecurityManager testSecurityManager = new DisallowExitSecurityManager(originalSecurityManager);
System.setSecurityManager(testSecurityManager);
}
@Override
protected void finished(Description description) {
System.setSecurityManager(originalSecurityManager);
}
}
Any test can be run with this Rule
by including it in the test class:
public class ExitWithCodeTest {
@Rule public final SystemExitRule systemExitRule = new SystemExitRule();
@Rule public ExpectedException exceptionRule = ExpectedException.none();
@Test
public void argZero_exitsWithExitCodeZero() {
exceptionRule.expect(SystemExitException.class);
ExitWithCode.main(new String[]{"0"});
}
}
See the junit4 branch of the system-exit demo on GitHub for the full example code.
Testing System.exit() with JUnit 5
JUnit 5 replaced the Rule mechanism with an extension mechanism – a more generalised way to customise test behaviours. We can register our SecurityManager
with an extension:
public class SystemExitExtension implements BeforeEachCallback, AfterEachCallback {
private SecurityManager originalSecurityManager;
@Override
public void beforeEach(ExtensionContext extensionContext) {
originalSecurityManager = System.getSecurityManager();
DisallowExitSecurityManager testSecurityManager = new DisallowExitSecurityManager(originalSecurityManager);
System.setSecurityManager(testSecurityManager);
}
@Override
public void afterEach(ExtensionContext extensionContext) {
System.setSecurityManager(originalSecurityManager);
}
}
We then extend our tests by using the @ExtendWith annotation:
@ExtendWith(SystemExitExtension.class)
class ExitWithCodeTest {
@Test
public void argZero_exitsWithExitCodeZero() {
SystemExitException exitException = assertThrows(SystemExitException.class, () ->
ExitWithCode.main(new String[]{"0"})
);
assertEquals(0, exitException.getStatusCode());
}
}
See the junit5 branch of the system-exit demo on GitHub for the full example code.
SecurityManager deprecation
A word of warning. This method works well up to JDK 17 but as of that version SecurityManager
is deprecated with no direct replacement. At time of writing (JDK 19) it’s still available to use but be aware that it is subject to removal in a future Java release. In JEP 411 which lists the rationale for the deprecation, it explicitly lists blocking System::exit
as a use case that a replacement mechanism should address.
Problem with all these is that securitymanager intercepts the actual system.exit.
So system.exit never occurs and also not the associated actions like removing temp files.
Therefore these methods are powerless to test if the system.exit actually works properly. (eg checking whether tmp files were actually removed etc)