我有一些方法应该在某些输入上调用System.exit()
。不幸的是,测试这些情况会导致JUnit终止!将方法调用放在新线程中似乎没有帮助,因为System.exit()
终止了JVM,而不仅仅是当前线程。是否有任何常见的处理方式?例如,我可以替换System.exit()
的存根吗?
[编辑]有问题的类实际上是一个命令行工具,我试图在JUnit中测试。也许JUnit根本不适合这份工作?建议使用补充回归测试工具(最好是与JUnit和EclEmma完美集成的东西)。
答案 0 :(得分:193)
确实,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建议in the comments使用System Rules,这是一组JUnit(4.9+)规则,用于测试使用java.lang.System
的代码。
最初在2011年12月的Stefan Birkner中由 his answer 提及。
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);
}
}
答案 1 :(得分:89)
库System Rules有一个名为ExpectedSystemExit的JUnit规则。使用此规则,您可以测试调用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);
}
}
完全披露:我是该图书馆的作者。
答案 2 :(得分:31)
您实际上可以在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)");
}
答案 3 :(得分:29)
如何在此方法中注入“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()以及退出代码。
答案 4 :(得分:20)
我们在代码库中使用的一个技巧是将对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());
}
答案 5 :(得分:5)
我喜欢已经给出的一些答案,但我想展示一种在获取待测遗留代码时通常很有用的不同技术。给出如下代码:
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;
}
}
这是将行为替换为测试用例的通用技术,我在重构遗留代码时一直使用它。它通常不是我要离开的地方,而是获得现有代码的中间步骤。
答案 6 :(得分:5)
我同意EricSchaefer。但是如果你使用像Mockito那样的好的模拟框架,一个简单的具体类就足够了,不需要接口和两个实现。
<强>问题:强>
// 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);
}
}
测试:
{{1}}
答案 7 :(得分:3)
您可以使用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);
答案 8 :(得分:3)
对于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);
}
答案 9 :(得分:3)
快速浏览一下api,可以看出System.exit可以抛出异常esp。如果安全管理员禁止关闭虚拟机。也许解决方案是安装这样的经理。
答案 10 :(得分:2)
有些环境中调用程序使用返回的退出代码(例如MS Batch中的ERRORLEVEL)。我们在代码中围绕主要方法进行了测试,我们的方法是使用与此处其他测试中使用的类似的SecurityManager覆盖。
昨晚我使用Junit @Rule注释整理了一个小JAR来隐藏安全管理器代码,并根据预期的返回代码添加期望。 http://code.google.com/p/junitsystemrules/
答案 11 :(得分:2)
大多数解决方案将
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()
的自定义消息超载。
答案 12 :(得分:2)
您可以使用替换运行时实例来测试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);
}
}
答案 13 :(得分:1)
SecurityManager
解决方案存在一个小问题。某些方法(例如JFrame.exitOnClose
)也会调用SecurityManager.checkExit
。在我的应用程序中,我不希望该调用失败,所以我使用了
Class[] stack = getClassContext();
if (stack[1] != JFrame.class && !okToExit) throw new ExitException();
super.checkExit(status);
答案 14 :(得分:1)
使用Runtime.exec(String command)
在单独的进程中启动JVM。
答案 15 :(得分:1)
调用System.exit()是一种不好的做法,除非它在main()中完成。这些方法应抛出一个异常,最终由main()捕获,然后使用适当的代码调用System.exit。
答案 16 :(得分:1)
可用于单元和集成测试的一种普遍有用的方法是拥有一个包私有(默认访问)可模拟运行器类,该类提供 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
答案 17 :(得分:0)
系统存根-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);