我有一些方法应该在某些输入上调用 System.exit()
。不幸的是,测试这些情况会导致 JUnit 终止!将方法调用放在新线程中似乎没有帮助,因为 System.exit()
终止了 JVM,而不仅仅是当前线程。是否有任何常见的模式来处理这个问题?例如,我可以用存根代替 System.exit()
吗?
[编辑] 有问题的类实际上是一个命令行工具,我试图在 JUnit 中进行测试。也许 JUnit 根本就不是适合这项工作的工具?欢迎提出补充回归测试工具的建议(最好是与 JUnit 和 EclEmma 很好集成的工具)。
System.exit()
不好的人 - 你的程序应该很快失败。在开发人员想要退出并给出虚假错误的情况下,抛出异常只会延长应用程序处于无效状态的时间。
实际上,Derkeiler.com 建议:
为什么 System.exit() ?
与其以 System.exit(whateverValue) 终止,不如抛出一个未经检查的异常?在正常使用中,它会一直漂移到 JVM 的最后一搏捕手并关闭您的脚本(除非您决定在途中的某个地方捕获它,这可能在某天有用)。在 JUnit 场景中,它将被 JUnit 框架捕获,该框架将报告某某测试失败并顺利进行下一个测试。
防止 System.exit() 实际退出 JVM:
尝试修改 TestCase 以使用阻止调用 System.exit 的安全管理器运行,然后捕获 SecurityException。
public class NoExitTestCase extends TestCase
{
protected static class ExitException extends SecurityException
{
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);
}
}
@Override
protected void setUp() throws Exception
{
super.setUp();
System.setSecurityManager(new NoExitSecurityManager());
}
@Override
protected void tearDown() throws Exception
{
System.setSecurityManager(null); // or save and restore original
super.tearDown();
}
public void testNoExit() throws Exception
{
System.out.println("Printing works");
}
public void testExit() throws Exception
{
try
{
System.exit(42);
} catch (ExitException e)
{
assertEquals("Exit status", 42, e.status);
}
}
}
2012 年 12 月更新:
Will 使用 System Rules 提出 in the comments,这是一组用于测试使用 java.lang.System
的代码的 JUnit(4.9+) 规则。
Stefan Birkner 在 { 5} 2011 年 12 月。
System.exit(…)
使用 ExpectedSystemExit 规则来验证 System.exit(...) 是否被调用。您也可以验证退出状态。
例如:
public void MyTest {
@Rule
public final ExpectedSystemExit exit = ExpectedSystemExit.none();
@Test
public void noSystemExit() {
//passes
}
@Test
public void systemExitWithArbitraryStatusCode() {
exit.expectSystemExit();
System.exit(0);
}
@Test
public void systemExitWithSelectedStatusCode0() {
exit.expectSystemExitWithStatus(0);
System.exit(0);
}
}
库 System Lambda 有一个方法 catchSystemExit
。使用此规则,您可以测试调用 System.exit(...) 的代码:
public class MyTest {
@Test
public void systemExitWithArbitraryStatusCode() {
SystemLambda.catchSystemExit(() -> {
//the code under test, which calls System.exit(...);
});
}
@Test
public void systemExitWithSelectedStatusCode0() {
int status = SystemLambda.catchSystemExit(() -> {
//the code under test, which calls System.exit(0);
});
assertEquals(0, status);
}
}
对于 Java 5 到 7,库 System Rules 有一个名为 ExpectedSystemExit 的 JUnit 规则。使用此规则,您可以测试调用 System.exit(...) 的代码:
public class MyTest {
@Rule
public final ExpectedSystemExit exit = ExpectedSystemExit.none();
@Test
public void systemExitWithArbitraryStatusCode() {
exit.expectSystemExit();
//the code under test, which calls System.exit(...);
}
@Test
public void systemExitWithSelectedStatusCode0() {
exit.expectSystemExitWithStatus(0);
//the code under test, which calls System.exit(0);
}
}
完全披露:我是这两个库的作者。
ExpectedSystemRule
的使用很好;问题是它需要一个额外的 3rd-party 库,它在现实世界的实用性方面提供的很少,并且是特定于 JUnit 的。
exit.checkAssertionAfterwards()
。
如何将“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() 以及使用哪个退出代码。
您实际上可以在 JUnit 测试中模拟或存根 System.exit
方法。
例如,使用 JMockit 您可以编写(还有其他方法):
@Test
public void mockSystemExit(@Mocked("exit") System mockSystem)
{
// Called by code under test:
System.exit(); // will not exit the program
}
编辑:替代测试(使用最新的 JMockit API)在调用 System.exit(n)
后不允许运行任何代码:
@Test(expected = EOFException.class)
public void checkingForSystemExitWhileNotAllowingCodeToContinueToRun() {
new Expectations(System.class) {{ System.exit(anyInt); result = new EOFException(); }};
// From the code under test:
System.exit(1);
System.out.println("This will never run (and not exit either)");
}
exit
调用之后运行(并不是说它真的是一个问题,IMO)。
Runtime
可以被模拟,但至少在我在 Gradle 中运行测试的场景中不会停止 System.exit
。
我们在代码库中使用的一个技巧是将 System.exit() 调用封装在 Runnable impl 中,该方法默认使用该方法。为了进行单元测试,我们设置了一个不同的模拟 Runnable。像这样的东西:
private static final Runnable DEFAULT_ACTION = new Runnable(){
public void run(){
System.exit(0);
}
};
public void foo(){
this.foo(DEFAULT_ACTION);
}
/* package-visible only for unit testing */
void foo(Runnable action){
// ...some stuff...
action.run();
}
...和 JUnit 测试方法...
public void testFoo(){
final AtomicBoolean actionWasCalled = new AtomicBoolean(false);
fooObject.foo(new Runnable(){
public void run(){
actionWasCalled.set(true);
}
});
assertTrue(actionWasCalled.get());
}
我喜欢已经给出的一些答案,但我想展示一种不同的技术,这种技术在测试遗留代码时通常很有用。给定如下代码:
public class Foo {
public void bar(int i) {
if (i < 0) {
System.exit(i);
}
}
}
您可以进行安全重构以创建包装 System.exit 调用的方法:
public class Foo {
public void bar(int i) {
if (i < 0) {
exit(i);
}
}
void exit(int i) {
System.exit(i);
}
}
然后你可以为你的测试创建一个假的,覆盖退出:
public class TestFoo extends TestCase {
public void testShouldExitWithNegativeNumbers() {
TestFoo foo = new TestFoo();
foo.bar(-1);
assertTrue(foo.exitCalled);
assertEquals(-1, foo.exitValue);
}
private class TestFoo extends Foo {
boolean exitCalled;
int exitValue;
void exit(int i) {
exitCalled = true;
exitValue = i;
}
}
这是一种用行为替换测试用例的通用技术,我在重构遗留代码时一直使用它。它通常不是我要离开的地方,而是让现有代码被测试的中间步骤。
对于 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);
}
创建一个包装 System.exit() 的模拟类
我同意EricSchaefer。但是如果你使用像 Mockito 这样好的模拟框架,一个简单的具体类就足够了,不需要一个接口和两个实现。
在 System.exit() 上停止测试执行
问题:
// do thing1
if(someCondition) {
System.exit(1);
}
// do thing2
System.exit(0)
模拟的 Sytem.exit()
不会终止执行。如果您想测试 thing2
未执行,这很糟糕。
解决方案:
您应该按照 martin 的建议重构此代码:
// do thing1
if(someCondition) {
return 1;
}
// do thing2
return 0;
并在调用函数中执行 System.exit(status)
。这会迫使您将所有 System.exit()
放在 main()
内或附近的一个地方。这比在你的逻辑深处调用 System.exit()
更干净。
代码
包装:
public class SystemExit {
public void exit(int status) {
System.exit(status);
}
}
主要的:
public class Main {
private final SystemExit systemExit;
Main(SystemExit systemExit) {
this.systemExit = systemExit;
}
public static void main(String[] args) {
SystemExit aSystemExit = new SystemExit();
Main main = new Main(aSystemExit);
main.executeAndExit(args);
}
void executeAndExit(String[] args) {
int status = execute(args);
systemExit.exit(status);
}
private int execute(String[] args) {
System.out.println("First argument:");
if (args.length == 0) {
return 1;
}
System.out.println(args[0]);
return 0;
}
}
测试:
public class MainTest {
private Main main;
private SystemExit systemExit;
@Before
public void setUp() {
systemExit = mock(SystemExit.class);
main = new Main(systemExit);
}
@Test
public void executeCallsSystemExit() {
String[] emptyArgs = {};
// test
main.executeAndExit(emptyArgs);
verify(systemExit).exit(1);
}
}
快速查看 api,表明 System.exit 可以抛出异常 esp。如果安全管理器禁止关闭虚拟机。也许解决方案是安装这样的管理器。
您可以使用 java SecurityManager 来防止当前线程关闭 Java VM。下面的代码应该做你想做的事:
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);
您可以通过替换 Runtime 实例来测试 System.exit(..)。例如使用 TestNG + Mockito:
public class ConsoleTest {
/** Original runtime. */
private Runtime originalRuntime;
/** Mocked runtime. */
private Runtime spyRuntime;
@BeforeMethod
public void setUp() {
originalRuntime = Runtime.getRuntime();
spyRuntime = spy(originalRuntime);
// Replace original runtime with a spy (via reflection).
Utils.setField(Runtime.class, "currentRuntime", spyRuntime);
}
@AfterMethod
public void tearDown() {
// Recover original runtime.
Utils.setField(Runtime.class, "currentRuntime", originalRuntime);
}
@Test
public void testSystemExit() {
// Or anything you want as an answer.
doNothing().when(spyRuntime).exit(anyInt());
System.exit(1);
verify(spyRuntime).exit(1);
}
}
系统存根 - https://github.com/webcompere/system-stubs - 也能够解决这个问题。它共享 System Lambda 的语法来包装我们知道将执行 System.exit
的代码,但是当其他代码意外退出时,这可能会导致奇怪的效果。
通过 JUnit 5 插件,我们可以确保任何退出都将转换为异常:
@ExtendWith(SystemStubsExtension.class)
class SystemExitUseCase {
// the presence of this in the test means System.exit becomes an exception
@SystemStub
private SystemExit systemExit;
@Test
void doSomethingThatAccidentallyCallsSystemExit() {
// this test would have stopped the JVM, now it ends in `AbortExecutionException`
// System.exit(1);
}
@Test
void canCatchSystemExit() {
assertThatThrownBy(() -> System.exit(1))
.isInstanceOf(AbortExecutionException.class);
assertThat(systemExit.getExitCode()).isEqualTo(1);
}
}
或者,也可以使用类似断言的静态方法:
assertThat(catchSystemExit(() -> {
//the code under test
System.exit(123);
})).isEqualTo(123);
System
。
在某些环境中,调用程序会使用返回的退出代码(例如 MS Batch 中的 ERRORLEVEL)。我们在代码中对执行此操作的主要方法进行了测试,我们的方法是使用与此处其他测试中使用的类似的 SecurityManager 覆盖。
昨晚我用 Junit @Rule 注释整理了一个小 JAR 来隐藏安全管理器代码,以及基于预期返回代码添加预期。 http://code.google.com/p/junitsystemrules/
大多数解决方案将
在调用 System.exit() 时终止测试(方法,而不是整个运行)
忽略已安装的 SecurityManager
有时非常特定于测试框架
限制每个测试用例最多使用一次
因此,大多数解决方案不适合以下情况:
副作用的验证将在调用 System.exit() 之后执行
现有的安全管理器是测试的一部分。
使用了不同的测试框架。
您希望在单个测试用例中进行多次验证。这可能是严格不推荐的,但有时会非常方便,尤其是与 assertAll() 结合使用时。
我对其他答案中提出的现有解决方案所施加的限制不满意,因此我自己想出了一些东西。
下面的类提供了一个方法 assertExits(int expectedStatus, Executable executable)
,它断言 System.exit()
是使用指定的 status
值调用的,并且测试可以在它之后继续。它的工作方式与 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 Rule
有改进的余地,例如,用可定制的消息重载 assertExits()
。
System.exit
到可捕获错误的通用转换,从而阻止意外退出停止测试。
使用 Runtime.exec(String command)
在单独的进程中启动 JVM。
SecurityManager
解决方案存在一个小问题。某些方法(例如 JFrame.exitOnClose
)也调用 SecurityManager.checkExit
。在我的应用程序中,我不希望那个调用失败,所以我使用了
Class[] stack = getClassContext();
if (stack[1] != JFrame.class && !okToExit) throw new ExitException();
super.checkExit(status);
可用于单元和集成测试的一种普遍有用的方法是拥有一个包私有(默认访问)可模拟运行器类,它提供 run() 和 exit() 方法。这些方法可以被测试模块中的 Mock 或 Fake 测试类覆盖。
测试类(JUnit 或其他)提供了 exit() 方法可以代替 System.exit() 抛出的异常。
package mainmocked;
class MainRunner {
void run(final String[] args) {
new MainMocked().run(args);
}
void exit(final int status) {
System.exit(status);
}
}
下面带有 main() 的类,也有一个 altMain() 来接收模拟或假跑步者,当单元或集成测试时:
package mainmocked;
public class MainMocked {
private static MainRunner runner = new MainRunner();
static void altMain(final String[] args, final MainRunner inRunner) {
runner = inRunner;
main(args);
}
public static void main(String[] args) {
try {
runner.run(args);
} catch (Throwable ex) {
// Log("error: ", ex);
runner.exit(1);
}
runner.exit(0);
} // main
public void run(String[] args) {
// do things ...
}
} // class
一个简单的模拟(使用 Mockito)将是:
@Test
public void testAltMain() {
String[] args0 = {};
MainRunner mockRunner = mock(MainRunner.class);
MainMocked.altMain(args0, mockRunner);
verify(mockRunner).run(args0);
verify(mockRunner).exit(0);
}
更复杂的测试类将使用 Fake,其中 run() 可以做任何事情,并使用 Exception 类来替换 System.exit():
private class FakeRunnerRuns extends MainRunner {
@Override
void run(String[] args){
new MainMocked().run(args);
}
@Override
void exit(final int status) {
if (status == 0) {
throw new MyMockExitExceptionOK("exit(0) success");
}
else {
throw new MyMockExitExceptionFail("Unexpected Exception");
} // ok
} // exit
} // class
这里的另一种技术是将附加代码引入(希望少数)逻辑执行 System.exit() 的地方。当逻辑作为单元测试的一部分被调用时,这个额外的代码可以避免执行 System.exit() 。例如,定义一个包私有常量,如 TEST_MODE,它通常为 false。然后让单元测试代码将此设置为 true 并向被测代码添加逻辑以检查该情况并绕过 System.exit 调用,而是抛出单元测试逻辑可以捕获的异常。顺便说一句,在 2021 年,使用像 spotbugs 这样的东西,这个问题可以表现在“java.io.IOException:现有连接被远程主机强行关闭”这个晦涩的错误中。
调用 System.exit() 是一种不好的做法,除非它在 main() 中完成。这些方法应该抛出一个异常,最终被你的 main() 捕获,然后用适当的代码调用 System.exit。
System.exit
。