Expand Authors Top

If you have a few years of experience in the Java ecosystem and you’d like to share that with the community, have a look at our Contribution Guidelines.

Expanded Audience – Frontegg – Security (partner)
announcement - icon User management is very complex, when implemented properly. No surprise here.

Not having to roll all of that out manually, but instead integrating a mature, fully-fledged solution - yeah, that makes a lot of sense.
That's basically what Frontegg is - User Management for your application. It's focused on making your app scalable, secure and enjoyable for your users.
From signup to authentication, it supports simple scenarios all the way to complex and custom application logic.

Have a look:

>> Elegant User Management, Tailor-made for B2B SaaS

November Discount Launch 2022 – Top
We’re finally running a Black Friday launch. All Courses are 30% off until tomorrow:

>> GET ACCESS NOW

November Discount Launch 2022 – TEMP TOP (NPI)
We’re finally running a Black Friday launch. All Courses are 30% off until tomorrow:

>> GET ACCESS NOW

1. Overview

In some situations, it may be required for a method to call System.exit() and shut down the application. For example, this could be if the application is supposed to run only once and then exit or in case of fatal errors like losing database connections.

If a method calls System.exit(), it becomes difficult to call it from unit tests and make assertions because that will cause the unit test to exit.

In this tutorial, we'll explore how to test methods that call System.exit() when using JUnit.

2. Project Setup

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

2.1. JUnit and Mockito Dependencies

Let's add the JUnit and Mockito dependencies:

<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

We'll start by adding an entity class called Task:

public class Task {
    private String name;

    // getters, setters and constructor
}

Next, let's create a DAO that is responsible for interacting with the database:

public class TaskDAO {
    public void save(Task task) throws Exception {
        // save the task
    }
}

The implementation of the save() method isn't important for the purpose of this article.

Next, let's create a TaskService that 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);
        }
    }
}

We should note that the application exits if the save() method throws an exception.

2.3. Unit Testing

Let's try to write a unit test for our saveTask() method above:

@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 have mocked TaskDAO to throw an exception when the save() method is called. This will lead to the execution of the catch block of saveTask(), which calls System.exit().

If we run this test, we'll find it exits before it finishes:

Test case is skipped because the application exits before the test finishes

3. Workaround Using Security Manager (Before Java 17)

We can provide our security manager to prevent the unit test from exiting. Our security manager will prevent calls to System.exit() and throw an exception if the call takes place. We can then catch the thrown exception to make assertions. By default, Java doesn't use a security manager, and calls to all the System methods are allowed.

It's important to note that SecurityManager was deprecated in Java 17 and will throw an exception if used with Java 17 or later.

3.1. Security Manager

Let's look at the implementation of the security manager:

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 behaviors of this code:

  • The method checkPermission() needs to be overridden because the default implementation of the security manager throws an exception if System.exit() is called.
  • Whenever our code calls System.exit(), the checkExit() method of NoExitSecurityManager will intervene and throw an exception.
  • Any other exception could be thrown instead of RuntimeException as long as it's an unchecked exception.

3.2. Modifying the Test

The next step is to modify the test to use the SecurityManager implementation. We'll add the setUp() and tearDown() methods to set and remove the security manager when the test is run:

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

Finally, let's change the test case to catch the RuntimeException that will be thrown when System.exit() is called:

@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 the catch block to verify that the status of the exit message is the same as the exit code set by the DAO.

4. System Lambda Library

Another way to write the test is by using the System Lambda Library. This library assists with testing code that calls methods of the System class. We'll explore how to use this library to write our test.

4.1. Dependencies

Let's start by adding the system-lambda dependency:

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

4.2. Modifying the Test Case

Next, let's modify the test case. We'll wrap our original test code with the catchSystemExit() method of the library. This method will prevent the system from exiting and will instead return the exit code. We'll then assert 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

The JMockit library provides a way to mock the System class. We can use it to change the behavior of System.exit() and prevent the system from exiting. Let's explore how to do it.

5.1. Dependency

Let's add the JMockit dependency:

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

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

<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 initialize before JUnit. This way, all the test cases run through JMockit. If using older versions of JMockit, the initialization parameter is not required.

5.2. Modifying the Test

Let's modify the test to mock 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 that we can catch and assert just like in the security manager example from earlier.

6. Conclusion

In this article, we looked at how it can be difficult to use JUnit to test code that calls System.exit(). We then explored a way to work around it by adding a Security Manager. We also looked at the libraries System Lambda and JMockit, which provide easier ways to approach the problem.

As always, the code examples used in this article can be found over on GitHub.

November Discount Launch 2022 – Bottom
We’re finally running a Black Friday launch. All Courses are 30% off until tomorrow:

>> GET ACCESS NOW

Junit footer banner
guest
0 Comments
Inline Feedbacks
View all comments