我有一些方法,应该在某些输入上调用System.exit()。不幸的是,测试这些用例会导致JUnit终止!将方法调用放在新线程中似乎没有帮助,因为System.exit()终止JVM,而不仅仅是当前线程。有什么共同的模式来处理这个问题吗?例如,我可以用存根代替System.exit()吗?
[编辑]所讨论的类实际上是一个命令行工具,我试图在JUnit中测试它。也许JUnit根本不是适合这项工作的工具?欢迎对补充回归测试工具提出建议(最好能与JUnit和EclEmma很好地集成)。
我有一些方法,应该在某些输入上调用System.exit()。不幸的是,测试这些用例会导致JUnit终止!将方法调用放在新线程中似乎没有帮助,因为System.exit()终止JVM,而不仅仅是当前线程。有什么共同的模式来处理这个问题吗?例如,我可以用存根代替System.exit()吗?
[编辑]所讨论的类实际上是一个命令行工具,我试图在JUnit中测试它。也许JUnit根本不是适合这项工作的工具?欢迎对补充回归测试工具提出建议(最好能与JUnit和EclEmma很好地集成)。
当前回答
使用运行时。exec(字符串命令)在单独的进程中启动JVM。
其他回答
大多数解决方案都会
在System.exit()被调用时终止测试(方法,而不是整个运行) 忽略已经安装的SecurityManager 有时是非常特定于测试框架的 限制每个测试用例使用Max一次
因此,大多数解决方案不适合以下情况:
在调用System.exit()之后执行副作用的验证 现有的安全管理器是测试的一部分。 使用了不同的测试框架。 您希望在单个测试用例中有多个验证。严格来说,不建议这样做,但有时非常方便,特别是与assertAll()结合使用时。
我不满意其他答案中提出的现有解决方案所施加的限制,因此我自己想出了一些东西。
下面的类提供了一个方法assertExits(int expectedStatus, Executable Executable可执行文件),它断言System.exit()是用指定的状态值调用的,并且测试可以在它之后继续。它的工作方式与JUnit 5 assertThrows相同。它还尊重现有的安全管理器。
还有一个问题:当测试中的代码安装一个新的安全管理器时,它将完全替换测试设置的安全管理器。据我所知,所有其他基于securitymanager的解决方案都遇到了同样的问题。
import java.security.Permission;
import static java.lang.System.getSecurityManager;
import static java.lang.System.setSecurityManager;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
public enum ExitAssertions {
;
public static <E extends Throwable> void assertExits(final int expectedStatus, final ThrowingExecutable<E> executable) throws E {
final SecurityManager originalSecurityManager = getSecurityManager();
setSecurityManager(new SecurityManager() {
@Override
public void checkPermission(final Permission perm) {
if (originalSecurityManager != null)
originalSecurityManager.checkPermission(perm);
}
@Override
public void checkPermission(final Permission perm, final Object context) {
if (originalSecurityManager != null)
originalSecurityManager.checkPermission(perm, context);
}
@Override
public void checkExit(final int status) {
super.checkExit(status);
throw new ExitException(status);
}
});
try {
executable.run();
fail("Expected System.exit(" + expectedStatus + ") to be called, but it wasn't called.");
} catch (final ExitException e) {
assertEquals(expectedStatus, e.status, "Wrong System.exit() status.");
} finally {
setSecurityManager(originalSecurityManager);
}
}
public interface ThrowingExecutable<E extends Throwable> {
void run() throws E;
}
private static class ExitException extends SecurityException {
final int status;
private ExitException(final int status) {
this.status = status;
}
}
}
你可以像这样使用这个类:
@Test
void example() {
assertExits(0, () -> System.exit(0)); // succeeds
assertExits(1, () -> System.exit(1)); // succeeds
assertExits(2, () -> System.exit(1)); // fails
}
如果需要,代码可以很容易地移植到JUnit 4、TestNG或任何其他框架。唯一特定于框架的元素没有通过测试。这可以很容易地更改为与框架无关的内容(而不是Junit 4规则)
还有改进的空间,例如,使用可定制的消息重载assertExits()。
如何注入一个“ExitManager”到这个方法:
public interface ExitManager {
void exit(int exitCode);
}
public class ExitManagerImpl implements ExitManager {
public void exit(int exitCode) {
System.exit(exitCode);
}
}
public class ExitManagerMock implements ExitManager {
public bool exitWasCalled;
public int exitCode;
public void exit(int exitCode) {
exitWasCalled = true;
this.exitCode = exitCode;
}
}
public class MethodsCallExit {
public void CallsExit(ExitManager exitManager) {
// whatever
if (foo) {
exitManager.exit(42);
}
// whatever
}
}
生产代码使用ExitManagerImpl,测试代码使用ExitManagerMock,可以检查是否调用了exit()以及使用哪个退出代码。
Another technique here is to introduce additional code into the (hopefully small number of) places where the logic does the System.exit(). This additional code then avoids doing the System.exit() when the logic is being called as part of unit test. For example, define a package private constant like TEST_MODE which is normally false. Then have the unit test code set this true and add logic to the code under test to check for that case and bypass the System.exit call and instead throw an exception that the unit test logic can catch. By the way, in 2021 and using something like spotbugs, this problem can manifest itself in the obscure error that "java.io.IOException: An existing connection was forcibly closed by the remote host".
为了让VonC在JUnit 4上运行,我修改了如下代码
protected static class ExitException extends SecurityException {
private static final long serialVersionUID = -1982617086752946683L;
public final int status;
public ExitException(int status) {
super("There is no escape!");
this.status = status;
}
}
private static class NoExitSecurityManager extends SecurityManager {
@Override
public void checkPermission(Permission perm) {
// allow anything.
}
@Override
public void checkPermission(Permission perm, Object context) {
// allow anything.
}
@Override
public void checkExit(int status) {
super.checkExit(status);
throw new ExitException(status);
}
}
private SecurityManager securityManager;
@Before
public void setUp() {
securityManager = System.getSecurityManager();
System.setSecurityManager(new NoExitSecurityManager());
}
@After
public void tearDown() {
System.setSecurityManager(securityManager);
}
可以使用java SecurityManager防止当前线程关闭java虚拟机。下面的代码应该是你想要的:
SecurityManager securityManager = new SecurityManager() {
public void checkPermission(Permission permission) {
if ("exitVM".equals(permission.getName())) {
throw new SecurityException("System.exit attempted and blocked.");
}
}
};
System.setSecurityManager(securityManager);