testing methods that call System.exit()

1. Overview

In certain situations, we may want a method to call System.exit() and terminated the application. For example, in case the application needs to be started only once and then terminated, or in case of fatal errors such as loss of database connections.

If the method calls System.exit()calling it from unit tests and making assertions becomes difficult because it will cause the unit test to terminate.

In this post, we’ll look at how to test methods that call System.exit() using the framework JUnit.

2. Project setup

Let’s start by creating a Java project. We will create a service that saves tasks to a database. If saving tasks to the database would throw an exception, the service will call System.exit().

2.1. JUnit and Mockito dependencies

Let’s add dependencies JUnit and Mockito:

<dependencies>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>5.9.1</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-core</artifactId>
        <version>4.8.1</version>
        <scope>test</scope>
    </dependency>
</dependencies>

2.2. Code setup

Let’s start by adding an entity class called Task:

public class Task {
    private String name;

    // геттеры, сеттеры и конструктор
}

Next, let’s create DAOresponsible for interacting with the database:

public class TaskDAO {
    public void save(Task task) throws Exception {
        // сохраняем таск
    }
}

Method Implementation save() for the purposes of this article is not important.

Next, let’s create TaskServicewhich calls the DAO:

public class TaskService {

    private final TaskDAO taskDAO = new TaskDAO();

    public void saveTask(Task task) {
        try {
            taskDAO.save(task);
        } catch (Exception e) {
            System.exit(1);
        }
    }
}

It should be noted that the application ends its work if the method save() throws an exception.

2.3. Unit Testing

Let’s try to write a unit test for a method saveTask():

@Test
void givenDAOThrowsException_whenSaveTaskIsCalled_thenSystemExitIsCalled() throws Exception {
    Task task = new Task("test");
    TaskDAO taskDAO = mock(TaskDAO.class);
    TaskService service = new TaskService(taskDAO);
    doThrow(new NullPointerException()).when(taskDAO).save(task);
    service.saveTask(task);
}

We made a mock TaskDAO object so that it throws an exception when a method is called save(). This will execute the block catch functions saveTask()which calls System.exit().

When we run this test, we find that it terminates before it has run to the end:

3. Security Manager workaround (before Java 17)

To avoid unit test termination, one can use security manager. Security Manager will prevent calls System.exit(), and if the call succeeds, it will throw an exception. You can then catch the thrown exception to assert. The Security Manager is not used by default in Java, and calls to all methods System allowed.

It is important to note that the SecurityManager is deprecated in Java 17 and will throw exceptions when used with Java 17 or later.

3.1. security manager

Let’s look at the Security Manager implementation:

class NoExitSecurityManager extends SecurityManager {
    @Override
    public void checkPermission(Permission perm) {
    }

    @Override
    public void checkExit(int status) {
        super.checkExit(status);
        throw new RuntimeException(String.valueOf(status));
    }
}

Let’s talk about a few important properties of this code:

  • Method checkPermission() must be overridden because the default implementation of the Security Manager in case of a call System.exit() throws an exception.

  • Whenever the code calls System.exit()method checkExit() class NoExitSecurityManager will throw an exception.

  • Instead of RuntimeException any other exception may be thrown, as long as it is an unchecked exception.

3.2. Test modification

The next step is to modify the test to use the implementation SecurityManager. We will add methods setUp() and tearDown() to install and uninstall Security Manager while running a test:

@BeforeEach
void setUp() {
    System.setSecurityManager(new NoExitSecurityManager());
}

Finally, let’s change the test case to intercept RuntimeExceptionwhich will be thrown when called System.exit():

@Test
void givenDAOThrowsException_whenSaveTaskIsCalled_thenSystemExitIsCalled() throws Exception {
    Task task = new Task("test");
    TaskDAO taskDAO = mock(TaskDAO.class);
    TaskService service = new TaskService(taskDAO);
    try {
        doThrow(new NullPointerException()).when(taskDAO).save(task);
        service.saveTask(task);
    } catch (RuntimeException e) {
         Assertions.assertEquals("1", e.getMessage());
    }
}

We use block catchto make sure the result message matches the return code set by the DAO.

4. System Lambda Library

You can also write a test using the library System Lambda. This library helps to test code that calls class methods. System. Let’s see how to use it to write our test.

4.1. Dependencies

Let’s start by adding a dependency system-lambda:

<dependency>
    <groupId>com.github.stefanbirkner</groupId>
    <artifactId>system-lambda</artifactId>
    <version>1.2.1</version>
    <scope>test</scope>
</dependency>

4.2. Test case modification

Now let’s modify the test case. We will wrap our test source code with the method catchSystemExit(). This method will prevent the logout and return an exit code instead. We then confirm the exit code:

@Test
void givenDAOThrowsException_whenSaveTaskIsCalled_thenSystemExitIsCalled() throws Exception {
    int statusCode = catchSystemExit(() -> {
        Task task = new Task("test");
        TaskDAO taskDAO = mock(TaskDAO.class);
        TaskService service = new TaskService(taskDAO);
        doThrow(new NullPointerException()).when(taskDAO).save(task);
        service.saveTask(task);
    });
    Assertions.assertEquals(1, statusCode);
}

5. Using JMockit

framework jmockit provides the ability to mock the System. It can be used to change behavior System.exit() and prevent the application from shutting down. Let’s see how to do it.

5.1. Addiction

Let’s add a dependency jmockit:

<dependency>
    <groupId>org.jmockit</groupId>
    <artifactId>jmockit</artifactId>
    <version>1.49</version>
    <scope>test</scope>
</dependency>

Along with this, you need to add the JVM initialization parameter -javaagent for jmockit. For this we can use the plugin Maven Surefire:

<plugins>
    <plugin>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>2.22.2</version> 
        <configuration>
           <argLine>
               -javaagent:"${settings.localRepository}"/org/jmockit/jmockit/1.49/jmockit-1.49.jar
           </argLine>
        </configuration>
    </plugin>
</plugins>

This causes JMockit to be initialized before JUnit. Thus, all test cases are executed through JMockit. When using older versions of JMockit, the initialization parameter is not required.

5.2. Test modification

Let’s modify the test to simulate System.exit():

@Test
public void givenDAOThrowsException_whenSaveTaskIsCalled_thenSystemExitIsCalled() throws Exception {
    new MockUp<System>() {
        @Mock
        public void exit(int value) {
            throw new RuntimeException(String.valueOf(value));
        }
    };

    Task task = new Task("test");
    TaskDAO taskDAO = mock(TaskDAO.class);
    TaskService service = new TaskService(taskDAO);
    try {
        doThrow(new NullPointerException()).when(taskDAO).save(task);
        service.saveTask(task);
    } catch (RuntimeException e) {
        Assertions.assertEquals("1", e.getMessage());
    }
}

This will throw an exception, which we can catch and test, just like in the previous Security Manager example.

6. Conclusion

In this article, we looked at how difficult it can be to use JUnit to test code that calls System.exit(). We then went over how to solve this problem by adding a Security Manager. They also mentioned the System Lambda and JMockit libraries, which provide simpler ways to solve this problem.

Code samples used in this article can be found on GitHub.


Tomorrow night there will be an open session for beginner Java developers who want to learn how to use enum in their applications. In the lesson, we will talk about what enumerations, constructors, fields, methods, singleton are. Registration is open link For everyone.

Similar Posts

Leave a Reply