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


198

我有一些应调用System.exit()某些输入的方法。不幸的是,测试这些情况会导致JUnit终止!将方法调用放在新的Thread中似乎无济于事,因为System.exit()终止JVM不仅终止当前线程。有什么通用的模式可以处理吗?例如,我可以用存根替换System.exit()吗?

[编辑]有问题的类实际上是我要在JUnit中测试的命令行工具。也许JUnit根本不是适合该工作的工具?欢迎提出补充回归测试工具的建议(最好是与JUnit和EclEmma很好地集成在一起的工具)。


2
我很好奇为什么函数会调用System.exit()...
Thomas Owens

2
如果您正在调用退出应用程序的函数。例如,如果用户尝试执行某项任务,那么他们无权连续执行超过x次,则可以将其强制退出应用程序。
Elie

5
我仍然认为在那种情况下,应该有一种更好的方法而不是System.exit()从应用程序返回。
Thomas Owens

27
如果您要测试main(),则调用System.exit()非常有意义。我们有一个要求,即对错误的批处理过程中应与1和0。成功退出退出
马修·法威尔

5
我不同意说不System.exit()好的每个人-您的程序应该很快失败。在开发人员要退出并产生虚假错误的情况下,引发异常只会使应用程序处于无效状态。
Sridhar Sarnobat,

Answers:


216

实际上,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.SystemStefan 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);
    }
}

4
不喜欢第一个答案,但第二个答案很酷-我没有弄乱安全经理,并认为他们比这更复杂。但是,您如何测试安全管理器/测试机制。
Bill K

6
确保拆卸正确执行,否则,测试将在Eclipse之类的运行程序中失败,因为JUnit应用程序无法退出!:)
MetroidFan2002

6
我不喜欢使用安全管理器的解决方案。对我来说,似乎只是为了测试而已。
Nicolai Reuschling,2009年

6
如果您使用的是junit 4.7或更高版本,请让一个库处理捕获您的System.exit调用。系统规则- stefanbirkner.github.com/system-rules
威尔

6
“与其以System.exit(whateverValue)终止,为什么不抛出未经检查的异常?” -因为我使用的命令行参数处理框架会System.exit在提供无效的命令行参数时调用。
亚当·帕金

106

系统规则具有一个称为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);
    }
}

完全公开:我是那个图书馆的作者。


3
完善。优雅。不必更改原始代码的痕迹,也不必与安全管理器一起玩。这应该是最佳答案!
2012年

2
这应该是最佳答案。直截了当,而不是就如何正确使用System.exit进行无休止的辩论。另外,这是一种灵活的解决方案,可遵循JUnit本身,因此我们无需重新发明轮子或与安全管理器混为一谈。
L. Holanda

@LeoHolanda此解决方案不会遇到您用来证明对我的回答投反对票的相同“问题”吗?另外,TestNG用户呢?
罗杰里奥2015年

2
@LeoHolanda你是对的;查看规则的实现,我现在看到它使用了一个自定义的安全管理器,该管理器在调用“系统出口”检查时引发了异常,因此结束了测试。我的答案中添加的示例测试满足了BTW的两个要求(使用System.exit被调用/未调用进行正确的验证)。ExpectedSystemRule当然,使用很好。问题在于它需要一个额外的第三方库,该库在实际使用方面几乎没有提供,并且是JUnit特定的。
罗杰里奥2015年

2
exit.checkAssertionAfterwards()
Stefan Birkner,2017年

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

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


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

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
}


编辑:替代测试(使用最新的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调用后运行(IMO确实不是问题)。
罗杰里奥2015年

用assert语句替换最后一个System.out.println()更合适吗?
user515655

“ @Mocked(” exit“)”在JMockit 1.43中似乎不再起作用:“注释类型为Mocked的属性值未定义”。docs:“在声明模拟字段和参数:@Mocked,它将在模拟类的所有现有和将来实例上模拟所有方法和构造函数(在使用它的测试期间);” jmockit.github.io/tutorial/Mocking.html#mocked
ThorstenSchöning

您实际上尝试过您的建议吗?我得到:Runtime可以模拟“ java.lang.IllegalArgumentException:类java.lang.System不可模拟” ,但System.exit至少在我在Gradle中运行Tests的情况下,它不会停止。
ThorstenSchöning'18年

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

他们称之为依赖注入吗?
托马斯·阿勒

2
如上所示,该示例有点半生的依赖项注入-依赖项传递给程序包可见的foo方法(通过公共foo方法或单元测试),但是主类仍对默认的Runnable实现进行硬编码。
Scott Bale 2010年

6

创建一个包装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);
    }
}

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

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


2
调用exit()时,此技术不会停止控制流。改用Exception。
安德烈·弗朗西亚

3

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


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

嗯 System.exit文档专门说将调用checkExit(int),而不是name为“ exitVM”的checkPermission。我想知道我是否应该同时覆盖这两者?
克里斯·康威,

权限名称实际上似乎是exitVM。(状态代码),即exitVM.0-至少在我最近的OSX测试中。
Mark Derricutt

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

3

您可以通过替换运行时实例来测试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);
    }
}

2

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

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


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()可自定义消息的超载。


1

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


3
如果它与单元测试不在同一个过程中,您将如何与被测类交互?
DNA

1
在99%的情况下,System.exit位于主方法内部。因此,您可以通过命令行参数(即args)与它们进行交互。
L. Holanda

1

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

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

0

除非在main()内部完成,否则调用System.exit()是一种不好的做法。这些方法应该引发异常,最终由您的main()捕获该异常,然后main()使用适当的代码调用System.exit。


8
但是,这并不能回答问题。如果要测试的功能最终是主要方法,该怎么办?因此,从设计角度来看,调用System.exit()可能是有效且可以的。您如何为此编写测试用例?
Elie

3
您不必测试main方法,因为main方法应该只接受任何参数,将它们传递给解析器方法,然后启动应用程序。要测试的主要方法应该没有逻辑。
Thomas Owens

5
@Elie:在这些类型的问题中,有两个有效答案。一个回答提出的问题,另一个问为什么提出这个问题。两种类型的答案都可以提供更好的理解,尤其是两者在一起时。
runaros
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.