System.exit()
。不幸的是,测试这些情况会导致JUnit终止!将方法调用放入新线程似乎没有帮助,因为System.exit()
终止JVM,而不仅仅是当前线程。有没有常见的处理方法?例如,我可以替换System.exit()
的存根吗?涉及的类实际上是一个命令行工具,我试图在JUnit中进行测试。也许JUnit并不是合适的工具?欢迎提供补充回归测试工具的建议(最好是与JUnit和EclEmma良好集成的工具)。
System.exit()
。不幸的是,测试这些情况会导致JUnit终止!将方法调用放入新线程似乎没有帮助,因为System.exit()
终止JVM,而不仅仅是当前线程。有没有常见的处理方法?例如,我可以替换System.exit()
的存根吗?确实,Derkeiler.com 提出了以下建议:
System.exit()
?与其使用 System.exit(任意值) 来终止程序,为什么不抛出一个未检查的异常呢?在正常使用中,它会一直传递到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提议在评论中使用{{link2:系统规则}},这是用于测试使用java.lang.System
的代码的JUnit(4.9+)规则集。
最初由{{link4:Stefan Birkner}}在2011年12月的{{link5:回答}}中提到。
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);
}
}
System.exit(whateverValue)
来结束程序?因为我正在使用命令行参数处理框架,每当提供无效的命令行参数时,它都会调用System.exit
。 - Adam Parkin库System Lambda有一个名为catchSystemExit
的方法。通过这个规则,您可以测试调用System.exit(...)的代码:
public class MyTest {
@Test
public void systemExitWithArbitraryStatusCode() {
SystemLambda.catchSystemExit(() -> {
//the code under test, which calls System.exit(...);
});
}
@Test
public void systemExitWithSelectedStatusCode0() {
int status = SystemLambda.catchSystemExit(() -> {
//the code under test, which calls System.exit(0);
});
assertEquals(0, status);
}
}
对于Java 5到7,库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(...);
}
@Test
public void systemExitWithSelectedStatusCode0() {
exit.expectSystemExitWithStatus(0);
//the code under test, which calls System.exit(0);
}
}
完全透明化:我是这两个库的作者。
ExpectedSystemRule
很好;问题是它需要一个额外的第三方库,提供非常少的实际用处,并且是特定于JUnit的。 - Rogérioexit.checkAssertionAfterwards()
。 - Stefan Birknerpublic 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()是否被调用以及使用哪个退出代码。在 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)");
}
exit
调用后运行代码(我认为这并不是问题)。 - Rogériojava.lang.System
不能再被mocked,但仍然可以通过MockUp
进行faked。 - RogérioSystem.exit()
。System Stubs库可以实现这一点:https://github.com/webcompere/system-stubs - Fr Jeremy Krieg我们在代码库中使用的一个技巧是将对System.exit()的调用封装在一个Runnable实现中,该方法默认使用。为了进行单元测试,我们设置了一个不同的模拟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());
}
系统存根 - 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);
为了让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);
}
我很喜欢已经给出的一些答案,但是我想展示一种不同的技术,当你需要对遗留代码进行测试时,这种技术通常很有用。假设有如下代码:
public class Foo {
public void bar(int i) {
if (i < 0) {
System.exit(i);
}
}
}
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;
}
}
这是一种通用的技术,可以用来将行为替换为测试用例。当我重构旧代码时,我经常使用它。通常情况下,我不会停留在这里,而是将其作为中间步骤,以使现有的代码能够进行测试。
我同意EricSchaefer的观点。但是如果你使用一个好的模拟框架,比如Mockito,一个简单的具体类就足够了,不需要接口和两个实现。
问题:
// 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);
}
}
System.exit()
是不好的人,因为你的程序应该快速失败。在开发者想要退出且会导致虚假错误的情况下,抛出异常只会延长应用程序处于无效状态的时间。 - Sridhar Sarnobat