Skip to content

Testing System.exit()

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:

JUnit result
In this case, JUnit has successfully completed two tests and then one of the remaining three terminated the JVM. Annoyingly, I can’t tell which one caused the termination.

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.

Published inHow ToTesting

One Comment

  1. nomail nomail

    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)

Leave a Reply

Your email address will not be published. Required fields are marked *