Java:如何testing调用System.exit()的方法?
我有几个方法应该在某些input上调用System.exit()
。 不幸的是,testing这些情况导致JUnit终止! 把方法调用放在一个新的线程中似乎没有什么帮助,因为System.exit()
终止了JVM,而不仅仅是当前的线程。 有没有处理这个问题的共同模式? 例如,我可以替代System.exit()
的存根吗?
[编辑]有问题的类实际上是一个命令行工具,我试图在JUnit里面testing。 也许JUnit根本就不是这个工作的正确工具? 对于互补回归testing工具的build议是受欢迎的(最好是与JUnit和EclEmma很好地结合的东西)。
事实上, Derkeiler.combuild议:
- 为什么
System.exit()
?
而不是终止与System.exit(whateverValue),为什么不抛出一个未经检查的exception? 在正常使用的情况下,它会一直漂移到JVM的最后一个捕手,并closures脚本(除非您决定在某处捕捉它,这可能在某一天有用)。
在JUnit场景中,它将被JUnit框架捕获,它将报告这样的testing失败并顺利移动到下一个。
- 防止
System.exit()
实际退出JVM:
尝试修改TestCase以防止调用System.exit的安全pipe理器运行,然后捕获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将 在使用系统规则 的注释中提出一组用于testing使用java.lang.System
代码的JUnit(4.9+)规则。
这是Stefan Birkner在2011年12月的回答中最初提到的。
System.exit(…)
使用
ExpectedSystemExit
规则来validation是否调用了System.exit(…)
。
你也可以validation退出状态。
例如:
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); } }
库系统规则有一个称为ExpectedSystemExit的JUnit规则。 有了这个规则,你就可以testing代码,调用System.exit(…):
public void 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); } }
充分披露:我是该图书馆的作者。
如何注入一个“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,testing代码使用ExitManagerMock,并可以检查是否调用了exit()以及使用哪个退出代码。
在JUnittesting中,您实际上可以模拟或者删除System.exit
方法。
例如,使用JMockit你可以写(也有其他的方式):
@Test public void mockSystemExit(@Mocked("exit") System mockSystem) { // Called by code under test: System.exit(); // will not exit the program }
编辑:替代testing(使用最新的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)"); }
我们在代码库中使用的一个技巧是将System.exit()调用封装在Runnable impl中,默认情况下使用该方法。 为了进行unit testing,我们设置了一个不同的模拟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(); }
…和JUnittesting方法…
public void testFoo(){ final AtomicBoolean actionWasCalled = new AtomicBoolean(false); fooObject.foo(new Runnable(){ public void run(){ actionWasCalled.set(true); } }); assertTrue(actionWasCalled.get()); }
创build一个包装System.exit()的模拟类
我同意EricSchaefer 。 但是如果你使用一个像Mockito这样的好的模拟框架,一个简单的具体类就足够了,不需要接口和两个实现。
停止对System.exit()的testing执行
问题:
// do thing1 if(someCondition) { System.exit(1); } // do thing2 System.exit(0)
Sytem.exit()
不会终止执行。 这是不好的,如果你想testingthing2
没有执行。
解:
你应该按照martin的build议重构这段代码:
// do thing1 if(someCondition) { return 1; } // do thing2 return 0;
在调用函数中做System.exit(status)
。 这迫使你将所有的System.exit()
放在main()
或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; } }
testing:
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); } }
我喜欢已经给出的一些答案,但是我想演示一种不同的技术,在获得遗留的testing代码时通常是有用的。 给定的代码如:
public class Foo { public void bar(int i) { if (i < 0) { System.exit(i); } } }
你可以做一个安全的重构来创build一个包装System.exit调用的方法:
public class Foo { public void bar(int i) { if (i < 0) { exit(i); } } void exit(int i) { System.exit(i); } }
那么你可以为你的testing创build一个假的覆盖退出:
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; } }
这是一种替代testing用例行为的通用技术,我在重构遗留代码时一直使用它。 它通常不是我要离开的地方,而是获取现有代码的中间步骤。
快速查看api,显示System.exit可以引发exception。 如果安全pipe理者禁止vm的closures。 也许解决办法是安装这样一个经理。
您可以使用java SecurityManager来防止当前线程closuresJava 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);
对于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()是一个糟糕的做法,除非它是在main()中完成的。 这些方法应该抛出一个exception,最终被main()所捕获,然后用相应的代码调用System.exit。
有一些环境返回的退出代码被调用程序使用(例如MS Batch中的ERRORLEVEL)。 我们已经在我们的代码中做了这些testing,并且我们的方法是使用类似于其他testing中使用的SecurityManager覆盖。
昨天晚上,我使用Junit @Rule注释来放置一个小JAR,以隐藏安全pipe理器代码,并根据预期的返回码添加期望值。 http://code.google.com/p/junitsystemrules/
您可以使用replace运行时实例来testingSystem.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); } }
使用Runtime.exec(String command)
在单独的进程中启动JVM。
SecurityManager
解决scheme存在一个小问题。 一些方法,如JFrame.exitOnClose
,也调用SecurityManager.checkExit
。 在我的申请中,我不想让这个电话失败,所以我用了
Class[] stack = getClassContext(); if (stack[1] != JFrame.class && !okToExit) throw new ExitException(); super.checkExit(status);