我有一些方法,应该在某些输入上调用System.exit()。不幸的是,测试这些用例会导致JUnit终止!将方法调用放在新线程中似乎没有帮助,因为System.exit()终止JVM,而不仅仅是当前线程。有什么共同的模式来处理这个问题吗?例如,我可以用存根代替System.exit()吗?

[编辑]所讨论的类实际上是一个命令行工具,我试图在JUnit中测试它。也许JUnit根本不是适合这项工作的工具?欢迎对补充回归测试工具提出建议(最好能与JUnit和EclEmma很好地集成)。


当前回答

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

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

其他回答

我们在代码库中使用的一个技巧是将对System.exit()的调用封装在Runnable impl中,该方法默认使用该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()); 
}

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

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

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

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

问题:

// do thing1
if(someCondition) {
    System.exit(1);
}
// do thing2
System.exit(0)

模拟的system .exit()不会终止执行。如果你想测试thing2没有被执行,这是很糟糕的。

解决方案:

你应该按照martin的建议重构这段代码:

// do thing1
if(someCondition) {
    return 1;
}
// do thing2
return 0;

在调用函数中执行System.exit(status)。这迫使你将所有System.exit()放在main()中或靠近main()的一个地方。这比在逻辑深处调用System.exit()更简洁。

Code

包装:

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

Another technique here is to introduce additional code into the (hopefully small number of) places where the logic does the System.exit(). This additional code then avoids doing the System.exit() when the logic is being called as part of unit test. For example, define a package private constant like TEST_MODE which is normally false. Then have the unit test code set this true and add logic to the code under test to check for that case and bypass the System.exit call and instead throw an exception that the unit test logic can catch. By the way, in 2021 and using something like spotbugs, this problem can manifest itself in the obscure error that "java.io.IOException: An existing connection was forcibly closed by the remote host".

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

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