Java: Как проверить методы, которые вызывают System.exit()?

У меня есть несколько методов, которые должны называть System.exit() на определенных входах. К сожалению, тестирование этих случаев заставляет JUnit прекратить работу! Помещение вызовов метода в новый поток, похоже, не помогает, поскольку System.exit() завершает JVM, а не только текущий поток. Существуют ли общие шаблоны для решения этой проблемы? Например, могу ли я оставить заглушку для System.exit()?

[EDIT] Этот класс - это инструмент командной строки, который я пытаюсь протестировать внутри JUnit. Может быть, JUnit - это просто не лучший инструмент для работы? Предложения по дополнительным инструментам тестирования регрессии приветствуются (желательно то, что хорошо интегрируется с JUnit и EclEmma).

Ответ 1

Действительно, Derkeiler.com предлагает:

  • Почему System.exit()?

Вместо того, чтобы завершаться с помощью System.exit(whateverValue), почему бы не выбросить исключенное исключение? При нормальном использовании он будет дрейфовать до самого последнего ловушка JVM и закрыть ваш script вниз (если вы не решите поймать его где-то на этом пути, что может когда-нибудь быть полезным).

В сценарии JUnit он будет зависеть от структуры JUnit, которая сообщит, что такой-то тест не прошел и плавно переходил к следующему.

  • Предотвратить System.exit() для фактического выхода из JVM:

Попробуйте модифицировать TestCase для запуска с помощью диспетчера безопасности, который запрещает вызов System.exit, а затем catch 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:

Will предлагает в комментариях с помощью Системные правила - коллекция правил JUnit (4.9+) для тестирования кода, который использует java.lang.System.
Первоначально это упоминалось в Stefan Birkner в его ответе в декабре 2011 года.

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

Ответ 2

В библиотеке System Rules есть правило JUnit, называемое ExpectedSystemExit. С помощью этого правила вы можете протестировать код, который вызывает 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

Фактически вы можете высмеивать или заглушать метод System.exit в тесте JUnit.

Например, используя JMockit, вы можете написать (есть и другие способы):

@Test
public void mockSystemExit(@Mocked("exit") System mockSystem)
{
    // Called by code under test:
    System.exit(); // will not exit the program
}


EDIT: Альтернативный тест (с использованием новейшего API JMockit), который не позволяет запускать код после вызова 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)");
}

Ответ 4

Как насчет ввода "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() и с каким кодом выхода.

Ответ 5

Один трюк, который мы использовали в нашей базе кода, состоял в том, чтобы вызов System.exit() был инкапсулирован в Runnable impl, который по умолчанию использовался по умолчанию. Для unit test мы устанавливаем другой макет 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()); 
}

Ответ 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);
    }
}

Main:

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

Ответ 7

Мне нравятся некоторые из уже полученных ответов, но я хотел продемонстрировать другую технику, которая часто бывает полезной при тестировании устаревшего кода. С учетом кода:

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

Это общий метод замены поведения для тестовых случаев, и я использую его все время, когда рефакторинг устаревшего кода. Обычно я не собираюсь оставлять вещи, но это промежуточный шаг для проверки существующего кода.

Ответ 8

Быстрый просмотр api, показывает, что System.exit может вызывать исключение esp. если диспетчер безопасности запрещает завершение работы vm. Возможно, решение было бы установить такого менеджера.

Ответ 9

Вы можете использовать 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);

Ответ 10

Для ответа 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);
}

Ответ 11

Calling System.exit() - это плохая практика, если только она не используется внутри main(). Эти методы должны вызывать исключение, которое, в конечном счете, попадает на ваш основной(), который затем вызывает System.exit с соответствующим кодом.

Ответ 12

Существуют среды, в которых возвращаемый код выхода используется вызывающей программой (например, ERRORLEVEL в MS Batch). У нас есть тесты вокруг основных методов, которые делают это в нашем коде, и наш подход заключался в использовании аналогичного переопределения SecurityManager, используемого в других тестах здесь.

Вчера вечером я собрал небольшой JAR, используя аннотации Junit @Rule, чтобы скрыть код менеджера безопасности, а также добавить ожидания на основе ожидаемого кода возврата. http://code.google.com/p/junitsystemrules/

Ответ 13

Вы можете протестировать System.exit(..) с заменой экземпляра Runtime. Например. с 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);
    }
}

Ответ 14

Используйте Runtime.exec(String command) для запуска JVM в отдельном процессе.

Ответ 15

Существует небольшая проблема с решением SecurityManager. Некоторые методы, такие как JFrame.exitOnClose, также вызывают SecurityManager.checkExit. В моем приложении я не хотел, чтобы этот вызов терпел неудачу, поэтому я использовал

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