[java] Java : System.exit ()를 호출하는 메소드를 테스트하는 방법?

System.exit()특정 입력을 호출 해야하는 몇 가지 방법이 있습니다. 불행히도, 이러한 경우를 테스트하면 JUnit이 종료됩니다! 메소드 호출을 새 스레드에 넣는 것은 System.exit()현재 스레드뿐만 아니라 JVM을 종료하기 때문에 도움이되지 않는 것 같습니다 . 이 문제를 처리하는 데 일반적인 패턴이 있습니까? 예를 들어, 스텁을 대체 할 수 System.exit()있습니까?

[편집] 문제의 클래스는 실제로 JUnit 내에서 테스트하려는 명령 행 도구입니다. 아마도 JUnit이 그 일에 적합한 도구가 아닐 수도 있습니다. 보완 회귀 테스트 도구에 대한 제안을 환영합니다 (JUnit 및 EclEmma와 잘 통합 된 것).



답변

실제로 Derkeiler.com 은 다음 과 같이 제안합니다.

  • System.exit()?

System.exit (whateverValue)로 종료하는 대신 확인되지 않은 예외를 발생시키지 않는 이유는 무엇입니까? 정상적인 사용에서는 JVM의 마지막 도랑 포수로 표류하고 스크립트를 종료합니다 (어딘가에 어딘가에 스크립트를 잡기로 결정하지 않는 한 언젠가는 유용 할 수 있습니다).

JUnit 시나리오에서는 JUnit 프레임 워크에 의해 포착됩니다. JUnit 프레임 워크는 이러한 테스트가 실패했음을보고하고 다음 단계로 원활하게 이동합니다.

  • 방지는 System.exit()실제로 JVM을 종료합니다 :

System.exit 호출을 방해하는 보안 관리자로 실행되도록 TestCase를 수정 한 다음 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 월 업데이트 :

제안 코멘트에 사용하는 시스템 규칙 (4.9+) 규정 된 사용하는 코드를 테스트하기 위해, JUnit을의 컬렉션을 java.lang.System.
이것은 처음에 언급 한 스테판 버크 너그의 대답 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);
    }
}


답변

라이브러리 시스템 규칙 에는 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);
    }
}

전체 공개 : 저는 그 도서관의 저자입니다.


답변

이 메소드에 “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 ()가 호출되었는지 여부와 종료 코드를 확인할 수 있습니다.


답변

실제로 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)");
}


답변

코드베이스에서 사용한 한 가지 트릭은 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());
}


답변

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


답변

나는 이미 주어진 답변 중 일부를 좋아하지만 테스트중인 레거시 코드를 얻을 때 종종 유용한 다른 기술을 보여주고 싶었습니다. 주어진 코드 :

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

그런 다음 exit를 재정의하는 가짜 테스트를 만들 수 있습니다.

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

이것은 테스트 사례의 동작을 대체하는 일반적인 기술이며 레거시 코드를 리팩토링 할 때 항상 사용합니다. 일반적으로 내가 떠날 곳이 아니라 테스트중인 기존 코드를 얻는 중간 단계입니다.