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

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


当前回答

Another technique here is to introduce additional code into the (hopefully small number of) places where the logic does the System.exit(). This additional code then avoids doing the System.exit() when the logic is being called as part of unit test. For example, define a package private constant like TEST_MODE which is normally false. Then have the unit test code set this true and add logic to the code under test to check for that case and bypass the System.exit call and instead throw an exception that the unit test logic can catch. By the way, in 2021 and using something like spotbugs, this problem can manifest itself in the obscure error that "java.io.IOException: An existing connection was forcibly closed by the remote host".

其他回答

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);
    }
}

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

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

为了让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 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);
    }
}

完全披露:我是这两个库的作者。