如何测试调用System.exit()的方法?

237
我有几个方法应该在特定输入上调用System.exit()。不幸的是,测试这些情况会导致JUnit终止!将方法调用放入新线程似乎没有帮助,因为System.exit()终止JVM,而不仅仅是当前线程。有没有常见的处理方法?例如,我可以替换System.exit()的存根吗?
涉及的类实际上是一个命令行工具,我试图在JUnit中进行测试。也许JUnit并不是合适的工具?欢迎提供补充回归测试工具的建议(最好是与JUnit和EclEmma良好集成的工具)。

2
我很好奇为什么一个函数会调用System.exit()... - Thomas Owens
2
如果您调用了一个退出应用程序的函数。例如,如果用户尝试连续多次执行未经授权的任务,则强制将其退出应用程序。 - Elie
5
我认为在那种情况下,应该有一种更好的方式来退出应用程序,而不是使用System.exit()。 - Thomas Owens
40
如果你正在测试main()函数,那么调用 System.exit() 就很合理。我们有一个要求,在错误的情况下,批处理应该以1退出,在成功的情况下以0退出。 - Matthew Farwell
11
我不同意那些认为System.exit()是不好的人,因为你的程序应该快速失败。在开发者想要退出且会导致虚假错误的情况下,抛出异常只会延长应用程序处于无效状态的时间。 - Sridhar Sarnobat
3
@ThomasOwens 我很好奇你为什么认为System.exit是不好的或者不必要的。请告诉我如何从Java应用程序中返回一个带有退出代码20或30的批处理。 - Stunner
19个回答

239

确实,Derkeiler.com 提出了以下建议:

  • 为什么要使用 System.exit()

与其使用 System.exit(任意值) 来终止程序,为什么不抛出一个未检查的异常呢?在正常使用中,它会一直传递到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提议在评论中使用{{link2:系统规则}},这是用于测试使用java.lang.System的代码的JUnit(4.9+)规则集。
最初由{{link4:Stefan Birkner}}在2011年12月的{{link5:回答}}中提到。

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

4
不太喜欢第一个答案,但第二个相当不错——我以前没有涉及过安全管理器,认为它们比那复杂得多。不过,你如何测试安全管理器/测试机制呢? - Bill K
8
确保拆卸(tear down)正确执行,否则在类似Eclipse这样的运行程序中,您的测试将失败,因为JUnit应用程序无法退出! :) - MetroidFan2002
6
我不喜欢使用安全管理器的解决方案。对我来说,这似乎只是为了测试而进行的一种折衷方法。 - Nicolai Reuschling
7
如果你正在使用JUnit 4.7或更高版本,则可以使用一个库来处理捕获System.exit调用的操作。 System Rules - http://stefanbirkner.github.com/system-rules/ - Will
10
为什么不抛出一个未检查的异常,而不是使用System.exit(whateverValue)来结束程序?因为我正在使用命令行参数处理框架,每当提供无效的命令行参数时,它都会调用System.exit - Adam Parkin
显示剩余13条评论

136

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

完全透明化:我是这两个库的作者。


5
完美。优雅。我不必改变一丝原始代码或者调整安全管理器。这应该是最佳答案! - Will
4
这应该是最好的答案。不要无休止地讨论System.exit的正确使用方法,直截了当地表达。此外,要在遵循JUnit本身的前提下提供灵活的解决方案,这样我们就不需要重新发明轮子或与安全管理器搞混了。 - L. Holanda
@LeoHolanda 这个解决方案不是遇到了你之前用来反对我的回答的同样的“问题”吗?还有,TestNG 用户怎么办? - Rogério
2
@LeoHolanda 你说得对;看了一下规则的实现,我现在明白它使用了一个自定义的安全管理器,在进行“系统退出”检查调用时抛出异常,从而结束测试。顺便说一下,我在答案中添加的示例测试满足了两个要求(正确验证是否调用了System.exit),当然,使用ExpectedSystemRule很好;问题是它需要一个额外的第三方库,提供非常少的实际用处,并且是特定于JUnit的。 - Rogério
2
exit.checkAssertionAfterwards() - Stefan Birkner
显示剩余5条评论

33
如何向这个方法注入一个“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()是否被调用以及使用哪个退出代码。

1
+1 不错的解决方案。如果您正在使用Spring,则很容易实现,因为ExitManager变成了一个简单的组件。只需注意确保在exitManager.exit()调用后,您的代码不会继续执行。当使用模拟ExitManager测试代码时,在调用exitManager.exit后,代码实际上不会退出。 - Joman68

32

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

2
投票反对原因:这个解决方案的问题在于,如果System.exit不是代码中的最后一行(即在if条件内),代码将继续运行。 - L. Holanda
@LeoHolanda 添加了一个测试版本,防止在exit调用后运行代码(我认为这并不是问题)。 - Rogério
最后一个 System.out.println() 用 assert 语句替换是否更合适? - user515655
@ThorstenSchöning,第二个测试适用于旧版本的JMockit;在新版本中,java.lang.System不能再被mocked,但仍然可以通过MockUp进行faked - Rogério
你也可以使用mockito-inline来模拟System.exit()。System Stubs库可以实现这一点:https://github.com/webcompere/system-stubs - Fr Jeremy Krieg
显示剩余2条评论

21

我们在代码库中使用的一个技巧是将对System.exit()的调用封装在一个Runnable实现中,该方法默认使用。为了进行单元测试,我们设置了一个不同的模拟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()); 
}

这就是他们所说的依赖注入吗? - Thomas Ahle
2
这个例子写得有点不完整的依赖注入 - 依赖关系被传递给了包可见的foo方法(由公共的foo方法或单元测试),但主类仍然硬编码默认的Runnable实现。 - Scott Bale

5

系统存根 - 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);

1
@FrJeremyKrieg - 作为该库的作者,我可以向您保证它不会。您无法将Mockito静态模拟应用于“System”。 - Ashley Frieze
抱歉,我误读了你的简介!我删除了我的评论,以避免误导其他人! - Fr Jeremy Krieg
@FrJeremyKrieg 这很容易做到...话虽如此,安全管理器即将离开,因此我们将不得不想出一种新的解决方法来解决这个问题。欢迎提出想法并提交PR。始终在寻找库的新输入。 - Ashley Frieze
任何通用解决方案可能都涉及代码编织或代理,我认为。也许是 JMockit(我相信它可以模拟 System.exit())。 - Fr Jeremy Krieg
@FrJeremyKrieg https://github.com/jmockit/jmockit1 已经有2年没有更新了。我认为这不是一个可以继续使用的模拟库。 - Ashley Frieze

5

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

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

然后你可以创建一个虚拟对象,用于测试时覆盖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;
    }
}

这是一种通用的技术,可以用来将行为替换为测试用例。当我重构旧代码时,我经常使用它。通常情况下,我不会停留在这里,而是将其作为中间步骤,以使现有的代码能够进行测试。


2
使用异常而不是exit()来停止控制流程。 - Andrea Francia

5

创建一个可模拟的类来包装System.exit()

我同意EricSchaefer的观点。但是如果你使用一个好的模拟框架,比如Mockito,一个简单的具体类就足够了,不需要接口和两个实现。

在System.exit()上停止测试执行

问题:

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

3
快速查看API,可以发现System.exit可能会抛出异常,特别是如果安全管理器禁止关闭VM。也许解决方案是安装这样的管理器。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接