在JUnit测试中处理System.exit(0)

14

我正在为一个现有的Java Swing应用程序实现一些测试,以便在重构和扩展代码时能够安全地进行,而不会出现问题。我首先使用JUnit编写了一些单元测试,因为这似乎是最简单的入门方式,但现在我的重点是创建一些端到端测试,以对整个应用程序进行测试。

我通过将每个测试方法放入单独的测试用例中,并在Ant的任务中使用fork="yes"选项,在每个测试中都重新启动应用程序。然而,我想要实现为测试而存在的一些用例涉及用户退出应用程序,这会导致其中一个方法调用System.exit(0)。这被JUnit视为错误:junit.framework.AssertionFailedError: Forked Java VM exited abnormally

是否有办法告诉JUnit,以返回代码0退出是可以接受的?


4个回答

33

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(...)
         * with an arbitrary status
         */
    }

    @Test
    public void systemExitWithSelectedStatusCode0() {
        exit.expectSystemExitWithStatus(0);
        //the code under test, which calls System.exit(0)
    }
}

System Rules 至少需要 JUnit 4.9。

完整的披露:我是 System Rules 的作者。


1
非常好!System Rules非常适合测试命令行工具,这些工具通常会执行System.exit()、写入System.out等操作。 - Torsten Römer
1
很酷的东西。在一些示例中,我被导入(org.junit.contrib.java.lang.system.ExpectedSystemExit;)误导了。它不在https://mvnrepository.com/artifact/org.junit.contrib中,而是在https://mvnrepository.com/artifact/com.github.stefanbirkner/system-rules中。 - kecso

8

我处理这个问题的方法是安装一个安全管理器,在调用System.exit时抛出异常。然后有一段代码捕获异常并且不会使测试失败。

public class NoExitSecurityManager
    extends java.rmi.RMISecurityManager
{
    private final SecurityManager parent;

    public NoExitSecurityManager(final SecurityManager manager)
    {
        parent = manager;
    }

    public void checkExit(int status)
    {
        throw new AttemptToExitException(status);
    }

    public void checkPermission(Permission perm)
    {
    }
}

然后在代码中,类似这样:

catch(final Throwable ex)
{
    final Throwable cause;

    if(ex.getCause() == null)
    {
        cause = ex;
    }
    else
    {
        cause = ex.getCause();
    }

    if(cause instanceof AttemptToExitException)
    {
        status = ((AttemptToExitException)cause).getStatus();
    }
    else
    {
        throw cause;
    }
}

assertEquals("System.exit must be called with the value of " + expectedStatus, expectedStatus, status);

这很有效,而且FEST-Swing甚至提供了自己的NoExitSecurityManager。我认为这将是我的现阶段方法,因为它不需要更改现有的应用程序代码。唯一的缺点是应用程序具有UncaughtExceptionHandler,当安全管理器阻止退出调用时会弹出一个错误报告对话框。测试仍然通过,但是任何观看测试的人可能会得出应用程序已崩溃的印象。 - Ben

6

您可以将“系统退出”抽象出一个新的依赖项,这样在您的测试中,您可以使用一个记录了调用 exit(及其值)事实的伪造对象,但在真正的应用程序中使用调用 System.exit 的实现。


这是一个可能性。在进行任何更改之前,我希望尽可能多地准备测试套件,但我猜有时你只需要深入其中,进行一些小心的更改来使测试工作正常。 - Ben
@Ben:希望在这种情况下,这将是一个相对较小的更改 - 我肯定更愿意这样做,而不是与安全管理器搞来搞去。当然,您还需要确保应用程序的Swing线程适当终止等。 - Jon Skeet
看代码,可能比我现在有时间处理的要复杂一些,但无疑是一个很好的练习,可以应用《与遗留代码有效工作》中的教训。我不确定如何适当地终止Swing线程:只需处理打开的Swing窗口?在这个阶段,似乎更安全的做法是为每个测试分叉一个新的VM,这就是为什么我每个类只能进行一个测试的原因。不过,我很高兴接收到更好的建议。 - Ben
@Ben:我相信关闭所有打开的窗口就可以了。很久以前可能不行,但我现在相信可以了。 - Jon Skeet

4
如果有人需要JUnit 5的此功能,我已经编写了一个扩展程序来实现。这是一个简单的注释,您可以使用它来告诉测试用例期望退出状态码或特定的退出状态码。
例如,任何退出代码都可以:
public class MyTestCases { 

    @Test
    @ExpectSystemExit
    public void thatSystemExitIsCalled() {
        System.exit(1);
    }
}

如果我们想要寻找特定的代码:
public class MyTestCases {

    @Test
    @ExpectSystemExitWithStatus(1)
    public void thatSystemExitIsCalled() {
        System.exit(1);
    }
}

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