我有一些方法,应该在某些输入上调用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
其他回答
你可以用替换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);
}
}
如何注入一个“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()以及使用哪个退出代码。
实际上,您可以模拟或排除该系统。exit方法,在JUnit测试中。
例如,使用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)");
}
标准库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);
}
}
完全披露:我是这两个库的作者。
可以用于单元和集成测试的一种通常有用的方法是,提供一个包私有(默认访问)可嘲弄的运行器类,该类提供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