我有一些方法,应该在某些输入上调用System.exit()。不幸的是,测试这些用例会导致JUnit终止!将方法调用放在新线程中似乎没有帮助,因为System.exit()终止JVM,而不仅仅是当前线程。有什么共同的模式来处理这个问题吗?例如,我可以用存根代替System.exit()吗?

[编辑]所讨论的类实际上是一个命令行工具,我试图在JUnit中测试它。也许JUnit根本不是适合这项工作的工具?欢迎对补充回归测试工具提出建议(最好能与JUnit和EclEmma很好地集成)。


当前回答

系统存根(https://github.com/webcompere/system-stubs)也能够解决这个问题。它共享System Lambda的语法,用于包装我们知道将执行System的代码。退出,但当其他代码意外退出时,可能会导致奇怪的效果。

通过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);

其他回答

使用运行时。exec(字符串命令)在单独的进程中启动JVM。

在某些环境中,返回的退出代码由调用程序使用(例如MS批处理中的ERRORLEVEL)。我们对代码中执行此操作的主要方法进行了测试,我们的方法是使用类似于在这里的其他测试中使用的SecurityManager重写。

昨晚,我使用Junit @Rule注释组合了一个小JAR,以隐藏安全管理器代码,并根据预期的返回代码添加期望。http://code.google.com/p/junitsystemrules/

调用System.exit()是一种糟糕的做法,除非它是在main()中完成的。这些方法应该抛出一个异常,最终由main()捕获,然后调用System。使用适当的代码退出。

你可以用替换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);
    }
}

可以用于单元和集成测试的一种通常有用的方法是,提供一个包私有(默认访问)可嘲弄的运行器类,该类提供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