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

Chr*_*way 187 java junit multithreading unit-testing testability

我有一些方法可以调用System.exit()某些输入.不幸的是,测试这些情况会导致JUnit终止!将方法调用放在新线程中似乎没有帮助,因为System.exit()终止JVM,而不仅仅是当前线程.是否有任何常见的处理方式?例如,我可以替换存根System.exit()吗?

[编辑]有问题的类实际上是一个命令行工具,我试图在JUnit中测试.也许JUnit根本不适合这份工作?建议使用补充回归测试工具(最好是与JUnit和EclEmma完美集成的东西).

Von*_*onC 206

的确,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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

2012年12月更新:

在评论中使用系统规则提出一系列JUnit(4.9+)规则来测试使用的代码java.lang.System.
这是Stefan Birkner在2011年12月的回答中最初提到的.

System.exit(…)
Run Code Online (Sandbox Code Playgroud)

使用该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 Code Online (Sandbox Code Playgroud)

  • 确保拆除正确执行,否则,您的测试将在Eclipse等运行器中失败,因为JUnit应用程序无法退出!:) (6认同)
  • 我不喜欢使用安全管理器的解决方案.对我来说似乎只是为了测试它. (6认同)
  • 如果您使用的是junit 4.7或更高版本,请让库处理捕获您的System.exit调用.系统规则 - http://stefanbirkner.github.com/system-rules/ (6认同)
  • "而不是使用System.exit(whateverValue)终止,为什么不抛出未经检查的异常?" - 因为我正在使用命令行参数处理框架,只要提供了无效的命令行参数,它就会调用`System.exit`. (6认同)
  • 不喜欢第一个答案,但第二个答案非常酷 - 我没有与安全经理混淆,并认为他们比这更复杂.但是,您如何测试安全管理器/测试机制. (4认同)
  • 正是我在寻找好的答案. (2认同)
  • 投票:不喜欢第一点.我同意System.exit不是一个好习惯,但不是问题的答案.不喜欢第二点.搞乱java安全管理器与单元测试不一致.我喜欢第三点,但它只是来自Stefan Birkner的复制和粘贴答案如下.他的回答应该是被接受的,而不是这个. (2认同)

Ste*_*ner 99

系统规则库有一个名为ExpectedSystemExit的JUnit规则.使用此规则,您可以测试调用System.exit(...)的代码:

public void 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

完全披露:我是该图书馆的作者.

  • 完善.优雅.无需更改原始代码的针脚或使用安全管理器.这应该是最好的答案! (3认同)
  • 哇这正是我想要的,谢谢! (2认同)
  • 这应该是最佳答案。直截了当,而不是就如何正确使用System.exit进行无休止的辩论。另外,这是一种灵活的解决方案,可以遵循JUnit本身,因此我们不需要重新发明轮子,也不必搞砸安全管理器。 (2认同)
  • @LeoHolanda你是对的;查看规则的实现,我现在看到它使用了自定义安全管理器,该安全管理器在调用“系统出口”检查时引发了异常,因此结束了测试。我的答案中添加的示例测试满足了BTW的两个要求(使用System.exit被调用/未调用进行正确的验证)。当然,使用ExpectedSystemRule很好。问题在于它需要一个额外的第三方库,该库在实际使用方面几乎没有提供,并且是特定于JUnit的。 (2认同)
  • 有`exit.checkAssertionAfterwards()`。 (2认同)

Eri*_*fer 31

如何在此方法中注入"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
    }
}
Run Code Online (Sandbox Code Playgroud)

生产代码使用ExitManagerImpl,测试代码使用ExitManagerMock,可以检查是否调用了exit()以及退出代码.

  • 我非常喜欢这个解决方案. (3认同)

Rog*_*rio 31

实际上,您可以System.exit在JUnit测试中模拟或删除该方法.

例如,您可以使用JMockit编写(还有其他方法):

@Test
public void mockSystemExit(@Mocked("exit") System mockSystem)
{
    // Called by code under test:
    System.exit(); // will not exit the program
}
Run Code Online (Sandbox Code Playgroud)


编辑:替代测试(使用最新的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)");
}
Run Code Online (Sandbox Code Playgroud)

  • 注意原因:此解决方案的问题在于,如果System.exit不是代码中的最后一行(即if if condition),代码将继续运行. (2认同)

Sco*_*ale 20

我们在代码库中使用的一个技巧是将对System.exit()的调用封装在Runnable impl中,默认情况下使用该方法.为了单元测试,我们设置了一个不同的模拟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(); 
}
Run Code Online (Sandbox Code Playgroud)

...和JUnit测试方法......

public void testFoo(){   
  final AtomicBoolean actionWasCalled = new AtomicBoolean(false);   
  fooObject.foo(new Runnable(){
    public void run(){
      actionWasCalled.set(true);
    }   
  });   
  assertTrue(actionWasCalled.get()); 
}
Run Code Online (Sandbox Code Playgroud)

  • 这个编写的示例是一种半生不熟的依赖注入 - 依赖关系被传递给package-visible foo方法(通过public foo方法或单元测试),但主类仍然硬编码默认的Runnable实现. (2认同)

Are*_*rff 6

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

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

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

问题:

// do thing1
if(someCondition) {
    System.exit(1);
}
// do thing2
System.exit(0)
Run Code Online (Sandbox Code Playgroud)

模拟Sytem.exit()不会终止执行.如果你想测试thing2没有执行,这是不好的.

解:

您应该按照martin的建议重构此代码:

// do thing1
if(someCondition) {
    return 1;
}
// do thing2
return 0;
Run Code Online (Sandbox Code Playgroud)

System.exit(status)在调用函数中执行.这迫使你将所有的System.exit()s放在一个地方或附近main().这比System.exit()在逻辑内深处调用要干净得多.

包装:

public class SystemExit {

    public void exit(int status) {
        System.exit(status);
    }
}
Run Code Online (Sandbox Code Playgroud)

主要:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

测试:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)


Jef*_*ick 5

我喜欢已经给出的一些答案,但我想展示一种在获得测试遗留代码时通常很有用的不同技术.给出如下代码:

public class Foo {
  public void bar(int i) {
    if (i < 0) {
      System.exit(i);
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

您可以进行安全重构以创建包装System.exit调用的方法:

public class Foo {
  public void bar(int i) {
    if (i < 0) {
      exit(i);
    }
  }

  void exit(int i) {
    System.exit(i);
  }
}
Run Code Online (Sandbox Code Playgroud)

然后你可以为你的测试创建一个覆盖退出的假货:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

这是用于替换行为的测试用例的通用技术,我在重构遗留代码时一直使用它.它通常不是我要离开的地方,而是获得现有代码的中间步骤.

  • 当调用exit()时,此tecniques不会停止控制流.请改用异常. (2认同)

Jeo*_*uan 5

为了在 JUnit 4 上运行 VonC 的答案,我修改了代码如下

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 Code Online (Sandbox Code Playgroud)


Ash*_*eze 5

System Stubs - 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

或者,也可以使用类似断言的静态方法:

assertThat(catchSystemExit(() -> {
   //the code under test
   System.exit(123);
})).isEqualTo(123);
Run Code Online (Sandbox Code Playgroud)