我有一些方法,应该在某些输入上调用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