我有一些方法,应该在某些输入上调用System.exit()。不幸的是,测试这些用例会导致JUnit终止!将方法调用放在新线程中似乎没有帮助,因为System.exit()终止JVM,而不仅仅是当前线程。有什么共同的模式来处理这个问题吗?例如,我可以用存根代替System.exit()吗?
[编辑]所讨论的类实际上是一个命令行工具,我试图在JUnit中测试它。也许JUnit根本不是适合这项工作的工具?欢迎对补充回归测试工具提出建议(最好能与JUnit和EclEmma很好地集成)。
我有一些方法,应该在某些输入上调用System.exit()。不幸的是,测试这些用例会导致JUnit终止!将方法调用放在新线程中似乎没有帮助,因为System.exit()终止JVM,而不仅仅是当前线程。有什么共同的模式来处理这个问题吗?例如,我可以用存根代替System.exit()吗?
[编辑]所讨论的类实际上是一个命令行工具,我试图在JUnit中测试它。也许JUnit根本不是适合这项工作的工具?欢迎对补充回归测试工具提出建议(最好能与JUnit和EclEmma很好地集成)。
当前回答
可以用于单元和集成测试的一种通常有用的方法是,提供一个包私有(默认访问)可嘲弄的运行器类,该类提供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
一个简单的mock(使用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
其他回答
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在评论中建议使用系统规则,这是JUnit(4.9+)规则的集合,用于测试使用java.lang.System. Rules的代码。 Stefan Birkner最初在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);
}
}
我喜欢已经给出的一些答案,但我想演示一种不同的技术,这种技术在测试遗留代码时通常很有用。给出如下代码:
public class Foo {
public void bar(int i) {
if (i < 0) {
System.exit(i);
}
}
}
您可以进行安全重构来创建包装系统的方法。退出电话:
public class Foo {
public void bar(int i) {
if (i < 0) {
exit(i);
}
}
void exit(int i) {
System.exit(i);
}
}
然后你可以为你的测试创建一个覆盖exit的假的:
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;
}
}
这是一种用行为代替测试用例的通用技术,我在重构遗留代码时一直使用它。这通常不是我要留下东西的地方,而是测试现有代码的中间步骤。
SecurityManager解决方案有一个小问题。一些方法,如JFrame。exitOnClose,也调用SecurityManager.checkExit。在我的应用程序中,我不希望这个调用失败,所以我使用了
Class[] stack = getClassContext();
if (stack[1] != JFrame.class && !okToExit) throw new ExitException();
super.checkExit(status);
为了让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);
}
可以用于单元和集成测试的一种通常有用的方法是,提供一个包私有(默认访问)可嘲弄的运行器类,该类提供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
一个简单的mock(使用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