JUnit-测试调用System.exit()的方法

2023/05/09

1. 概述

在某些情况下,方法可能需要调用System.exit()并关闭应用程序。例如,这可能是应用程序应该只运行一次然后退出,或者发生致命错误(如丢失数据库连接)的情况。

如果方法调用System.exit(),则很难从单元测试中调用它并进行断言,因为这会导致单元测试退出。

在本教程中,我们将探讨如何在使用JUnit时测试调用System.exit()的方法。

2. 项目设置

让我们从创建一个Java项目开始。我们将创建一个将任务保存到数据库的服务。如果将任务保存到数据库引发异常,该服务将调用System.exit()。

2.1 JUnit和Mockito依赖项

让我们添加JUnitMockito依赖项:

<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 代码设置

我们将首先添加一个名为Task的实体类:

public class Task {
    private String name;

    // getters, setters and constructor
}

接下来,让我们创建一个负责与数据库交互的DAO

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

save()方法的实现对于本文的目的并不重要。

接下来,让我们创建一个调用DAO的TaskService:

public class TaskService {

    private final TaskDAO taskDAO = new TaskDAO();

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

我们应该注意,如果save()方法抛出异常,应用程序将退出

2.3 单元测试

让我们尝试为上面的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);
}

我们Mock了TaskDAO在调用save()方法时抛出异常。这将导致执行saveTask()的catch块,它调用System.exit()。

如果我们运行这个测试,我们会发现它在完成之前就退出了

3. 使用安全管理器的解决方法(Java 17之前)

我们可以提供我们的安全管理器来防止单元测试退出。我们的安全管理器将阻止对System.exit()的调用并在调用发生时抛出异常。然后我们可以捕获抛出的异常来进行断言。默认情况下,Java不使用安全管理器,并且允许调用所有System方法。

需要注意的是,SecurityManager在Java 17中已被弃用,如果与Java 17或更高版本一起使用,将会抛出异常

3.1 SecurityManager

我们看一下安全管理器的实现:

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));
    }
}

让我们谈谈这段代码的几个重要行为:

  • checkPermission()方法需要被重写,因为如果调用System.exit(),安全管理器的默认实现会抛出异常
  • 每当我们的代码调用System.exit()时,NoExitSecurityManager的checkExit()方法就会介入并抛出异常
  • 只要它是非受检的异常,就可以抛出任何其他异常而不是RuntimeException

3.2 修改测试

下一步是修改测试以使用SecurityManager实现。我们将添加setUp()和tearDown()方法来在测试运行时设置和删除安全管理器

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

最后,让我们更改测试用例以捕获调用System.exit()时将抛出的RuntimeException

@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());
    }
}

我们使用catch块来验证退出消息的状态是否与DAO设置的退出代码相同

4. System Lambda库

编写测试的另一种方法是使用System Lambda库。该库有助于测试调用System类方法的代码,我们将探讨如何使用这个库来编写我们的测试。

4.1 依赖关系

让我们从添加system-lambda依赖项开始:

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

4.2 修改测试用例

接下来,让我们修改测试用例。我们将使用库的catchSystemExit()方法包装我们的原始测试代码。此方法将阻止系统退出,而是返回退出代码。然后我们将断言退出代码:

@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. 使用JMockit

JMockit库提供了一种Mock System类的方法。我们可以用它来改变System.exit()的行为并防止系统退出

5.1 依赖项

让我们添加JMockit依赖项:

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

除此之外,我们需要为JMockit添加-javaagent JVM初始化参数。为此,我们可以使用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>

这会导致JMockit在JUnit之前初始化。这样,所有的测试用例都通过JMockit运行。如果使用旧版本的JMockit,则不需要初始化参数。

5.2 修改测试

让我们修改测试以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());
    }
}

这将抛出一个异常,我们可以像前面的安全管理器示例中那样捕获并断言该异常。

6. 总结

在本文中,我们了解了使用JUnit来测试调用System.exit()的代码是多么困难。然后,我们探索了一种通过添加安全管理器来解决它的方法。我们还研究了System Lambda和JMockit库,它们提供了解决问题的更简单方法。

与往常一样,本文中使用的代码示例可以在GitHub上找到。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章