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

时间:2008-11-21 16:38:59

标签: java multithreading unit-testing junit testability

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

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

18 个答案:

答案 0 :(得分:193)

确实,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建议in the comments使用System Rules,这是一组JUnit(4.9+)规则,用于测试使用java.lang.System的代码。
最初在2011年12月的Stefan Birkner中由 his answer 提及。

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

答案 1 :(得分:89)

System Rules有一个名为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);
    }
}

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

答案 2 :(得分:31)

您实际上可以在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)");
}

答案 3 :(得分:29)

如何在此方法中注入“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()以及退出代码。

答案 4 :(得分: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(); 
}

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

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

答案 5 :(得分: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);
  }
}

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

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

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

答案 6 :(得分: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);
    }
}

测试:

{{1}}

答案 7 :(得分:3)

您可以使用java SecurityManager来阻止当前线程关闭Java VM。以下代码应该执行您想要的操作:

SecurityManager securityManager = new SecurityManager() {
    public void checkPermission(Permission permission) {
        if ("exitVM".equals(permission.getName())) {
            throw new SecurityException("System.exit attempted and blocked.");
        }
    }
};
System.setSecurityManager(securityManager);

答案 8 :(得分:3)

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

答案 9 :(得分:3)

快速浏览一下api,可以看出System.exit可以抛出异常esp。如果安全管理员禁止关闭虚拟机。也许解决方案是安装这样的经理。

答案 10 :(得分:2)

有些环境中调用程序使用返回的退出代码(例如MS Batch中的ERRORLEVEL)。我们在代码中围绕主要方法进行了测试,我们的方法是使用与此处其他测试中使用的类似的SecurityManager覆盖。

昨晚我使用Junit @Rule注释整理了一个小JAR来隐藏安全管理器代码,并根据预期的返回代码添加期望。 http://code.google.com/p/junitsystemrules/

答案 11 :(得分:2)

大多数解决方案将

  • System.exit()被调用时终止测试(方法,而不是整个运行)
  • 忽略已安装的SecurityManager
  • 有时对于测试框架而言非常具体
  • 每个测试案例最多只能使用一次

因此,大多数解决方案都不适合以下情况:

  • 应在调用System.exit()后进行副作用验证
  • 现有的安全管理器是测试的一部分。
  • 使用了不同的测试框架。
  • 您希望在一个测试用例中进行多个验证。严格不建议这样做,但有时可能非常方便,例如与assertAll()结合使用。

我对其他答案中提出的现有解决方案施加的限制不满意,因此我自己想出了一些办法。

以下类提供方法assertExits(int expectedStatus, Executable executable),该方法断言已使用指定的System.exit()值调用了status,并且测试可以在此之后继续。它的工作方式与JUnit 5 assertThrows相同。它还尊重现有的安全经理。

还有一个问题:当被测代码安装新的安全管理器时,它将完全替换测试设置的安全管理器。我所知道的所有其他基于SecurityManager的解决方案都遇到相同的问题。

import java.security.Permission;

import static java.lang.System.getSecurityManager;
import static java.lang.System.setSecurityManager;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;

public enum ExitAssertions {
    ;

    public static <E extends Throwable> void assertExits(final int expectedStatus, final ThrowingExecutable<E> executable) throws E {
        final SecurityManager originalSecurityManager = getSecurityManager();
        setSecurityManager(new SecurityManager() {
            @Override
            public void checkPermission(final Permission perm) {
                if (originalSecurityManager != null)
                    originalSecurityManager.checkPermission(perm);
            }

            @Override
            public void checkPermission(final Permission perm, final Object context) {
                if (originalSecurityManager != null)
                    originalSecurityManager.checkPermission(perm, context);
            }

            @Override
            public void checkExit(final int status) {
                super.checkExit(status);
                throw new ExitException(status);
            }
        });
        try {
            executable.run();
            fail("Expected System.exit(" + expectedStatus + ") to be called, but it wasn't called.");
        } catch (final ExitException e) {
            assertEquals(expectedStatus, e.status, "Wrong System.exit() status.");
        } finally {
            setSecurityManager(originalSecurityManager);
        }
    }

    public interface ThrowingExecutable<E extends Throwable> {
        void run() throws E;
    }

    private static class ExitException extends SecurityException {
        final int status;

        private ExitException(final int status) {
            this.status = status;
        }
    }
}

您可以使用此类:

    @Test
    void example() {
        assertExits(0, () -> System.exit(0)); // succeeds
        assertExits(1, () -> System.exit(1)); // succeeds
        assertExits(2, () -> System.exit(1)); // fails
    }

如有必要,可以轻松地将代码移植到JUnit 4,TestNG或任何其他框架。唯一特定于框架的元素无法通过测试。可以轻松地将其更改为与框架无关的内容(除了 Junit 4 Rule

还有改进的空间,例如,assertExits()的自定义消息超载。

答案 12 :(得分:2)

您可以使用替换运行时实例来测试System.exit(..)。 例如。与TestNG + Mockito:

public class ConsoleTest {
    /** Original runtime. */
    private Runtime originalRuntime;

    /** Mocked runtime. */
    private Runtime spyRuntime;

    @BeforeMethod
    public void setUp() {
        originalRuntime = Runtime.getRuntime();
        spyRuntime = spy(originalRuntime);

        // Replace original runtime with a spy (via reflection).
        Utils.setField(Runtime.class, "currentRuntime", spyRuntime);
    }

    @AfterMethod
    public void tearDown() {
        // Recover original runtime.
        Utils.setField(Runtime.class, "currentRuntime", originalRuntime);
    }

    @Test
    public void testSystemExit() {
        // Or anything you want as an answer.
        doNothing().when(spyRuntime).exit(anyInt());

        System.exit(1);

        verify(spyRuntime).exit(1);
    }
}

答案 13 :(得分:1)

SecurityManager解决方案存在一个小问题。某些方法(例如JFrame.exitOnClose)也会调用SecurityManager.checkExit。在我的应用程序中,我不希望该调用失败,所以我使用了

Class[] stack = getClassContext();
if (stack[1] != JFrame.class && !okToExit) throw new ExitException();
super.checkExit(status);

答案 14 :(得分:1)

使用Runtime.exec(String command)在单独的进程中启动JVM。

答案 15 :(得分:1)

调用System.exit()是一种不好的做法,除非它在main()中完成。这些方法应抛出一个异常,最终由main()捕获,然后使用适当的代码调用System.exit。

答案 16 :(得分:1)

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

一个简单的模拟(使用 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

答案 17 :(得分:0)

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