概述
在某些情况下,方法可能需要调用 System.exit()
来关闭应用程序。例如,当应用只执行一次然后退出,或者在遇到致命错误如数据库连接丢失时。然而,如果一个方法调用了 System.exit()
,那么从单元测试中调用它并进行断言就会变得困难,因为这会导致测试也退出。
本文将指导您如何在使用 JUnit 进行测试时,处理调用 System.exit()
的方法。
1. 项目设置
首先,我们创建一个Java项目。我们将创建一个服务,用于将任务保存到数据库。如果保存任务时数据库抛出异常,服务将调用 System.exit()
。
1.1. JUnit 和 Mockito 依赖
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
</dependencies>
1.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()
方法的具体实现对本文目的不重要。
然后,创建一个 TaskService,调用 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);
}
}
}
值得注意的是,如果save()
方法抛出异常,应用将退出。
1.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);
}
我们已模拟了 TaskDAO
,使其在调用 save()
方法时抛出异常。这将导致执行 saveTask()
方法的 catch
块,其中调用了 System.exit()
。
运行此测试时,我们会发现它在完成前就退出了:
2. 使用安全管理器的工作绕行(Java 17 之前)
我们可以提供一个 安全管理器来阻止单元测试退出。 我们的安全管理器将阻止 System.exit()
调用,并在发生时抛出异常。然后我们可以捕获这个异常进行断言。默认情况下,Java 不使用安全管理器,允许对所有 System
方法的调用。
需要注意的是,SecurityManager
在 Java 17 及更高版本中已被弃用,使用时会抛出异常。
2.1. 安全管理器
让我们看看安全管理器的实现:
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()
方法将介入并抛出异常。 - 可以抛出任何类型的异常,只要它是未检查异常。
2.2. 修改测试
下一步是修改测试,使其使用安全管理器。我们将 在测试运行时添加和移除安全管理器 的 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 设置的退出代码相同。
3. 系统Lambda库
另一种编写测试的方法是使用 System Lambda Library。此库有助于测试调用 System
类方法的代码。我们将探讨如何利用此库编写测试。
3.1. 依赖项
首先,添加 system-lambda 的依赖:
<dependency>
<groupId>com.github.stefanbirkner</groupId>
<artifactId>system-lambda</artifactId>
<version>1.2.1</version>
<scope>test</scope>
</dependency>
3.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);
}
4. 使用 JMockit
JMockit 库提供了一种方式来模拟 System
类。我们可以利用它改变 System.exit()
的行为,防止系统退出。让我们看看如何操作。
4.1. 依赖
添加 JMockit 的依赖:
<dependency>
<groupId>org.jmockit</groupId>
<artifactId>jmockit</artifactId>
<version>1.49</version>
<scope>test</scope>
</dependency>
此外,我们需要在 JVM 初始化参数中添加 -javaagent
来启用 JMockit。可以使用 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 版本,不需要这个初始化参数。
4.2. 修改测试
接下来,修改测试以模拟 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());
}
}
这将抛出一个异常,我们可以捕获并像之前的安全管理器示例一样进行断言。
5. 结论
本文探讨了在使用 JUnit 测试时,如何处理调用 System.exit()
的代码变得困难。我们还介绍了如何通过添加安全管理器来解决这个问题,并查看了 System Lambda 和 JMockit 库,它们提供了更简单的方法来处理这个问题。
本文中的代码示例可在 GitHub 查看。