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

199

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

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

Крис Конвей
источник
2
Мне любопытно, почему функция будет вызывать System.exit () ...
Томас Оуэнс
2
Если вы вызываете функцию, которая выходит из приложения. Например, если пользователь пытается выполнить задачу, которую ему не разрешено выполнять более x раз подряд, вы вынуждаете его выйти из приложения.
Elie
5
Я все еще думаю, что в этом случае должен быть более хороший способ возврата из приложения, а не System.exit ().
Томас Оуэнс
28
Если вы тестируете main (), тогда имеет смысл вызвать System.exit (). У нас есть требование, чтобы при ошибке пакетный процесс завершился с 1, а при успешном завершении - с 0.
Мэтью Фарвелл,
5
Я не согласен со всеми, кто говорит, что System.exit()плохо - ваша программа должна быстро провалиться. Создание исключения просто продлевает приложение в недопустимом состоянии в ситуациях, когда разработчик хочет выйти и выдает ложные ошибки.
Шридхар Сарнобат

Ответы:

217

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

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

Вместо завершения с System.exit (whatValue), почему бы не бросить непроверенное исключение? При обычном использовании он будет полностью дойти до последнего ловца 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 года:

Уилл предлагает в комментариях с помощью правил системы , коллекция JUnit (4.9+) правила для тестирования кода , который использует java.lang.System.
Это было первоначально упомянуто Стефаном Биркнером в его ответе в декабре 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);
    }
}
VonC
источник
4
Не понравился первый ответ, но второй довольно крутой - я не связывался с менеджерами по безопасности и полагал, что они сложнее. Тем не менее, как вы тестируете менеджер безопасности / механизм тестирования.
Билл К
7
Убедитесь, что разборка выполняется правильно, иначе ваш тест не будет выполнен в Eclipse, потому что приложение JUnit не может выйти! :)
MetroidFan2002
6
Мне не нравится решение с использованием менеджера безопасности. Для меня это похоже на хак, просто чтобы проверить это.
Николай Рейшлинг
7
Если вы используете junit 4.7 или выше, позвольте библиотеке разобраться с вашими вызовами System.exit. Системные правила - stefanbirkner.github.com/system-rules
будет
6
«Вместо завершения с System.exit (whatValue), почему бы не бросить непроверенное исключение?» - потому что я использую инфраструктуру обработки аргументов командной строки, которая вызывает System.exitвсякий раз, когда предоставляется неверный аргумент командной строки.
Адам Паркин
108

В библиотеке System Lambda есть метод. С помощью catchSystemExitэтого правила вы можете тестировать код, который вызывает System.exit (...):

public void MyTest {
    @Test
    public void systemExitWithArbitraryStatusCode() {
        SystemLambda.catchSystemExit(() -> {
            //the code under test, which calls System.exit(...);
        });
    }
}

Для Java 5-7 в Системных правилах библиотеки есть правило 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
Отлично. Элегантный. Не пришлось менять стежок моего исходного кода или играть с менеджером безопасности. Это должен быть главный ответ!
Будет
2
Это должен быть главный ответ. Вместо бесконечных споров о правильном использовании System.exit, прямо к делу. Кроме того, гибкое решение в соответствии с самой JUnit, поэтому нам не нужно изобретать велосипед или связываться с менеджером безопасности.
Л. Холанда
@ LeoHolanda Разве это решение не страдает от той же "проблемы", которую вы использовали, чтобы оправдать понижение в моем ответе? А как насчет пользователей TestNG?
Rogério
2
@ LeoHolanda Вы правы; Глядя на реализацию правила, я теперь вижу, что он использует собственный менеджер безопасности, который выдает исключение, когда происходит вызов проверки «выхода из системы», поэтому тест завершается. Добавленный пример теста в моем ответе удовлетворяет обоим требованиям (правильная проверка с System.exit вызывается / не вызывается), кстати. Использование ExpectedSystemRule, конечно, приятно; проблема в том, что она требует дополнительной сторонней библиотеки, которая предоставляет очень мало с точки зрения реальной полезности, и является специфичной для JUnit.
Rogério
2
Есть exit.checkAssertionAfterwards().
Стефан Биркнер
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 () и с каким кодом выхода.

EricSchaefer
источник
1
+1 Отличное решение. Также легко реализовать, если вы используете Spring, поскольку ExitManager становится простым Компонентом. Просто имейте в виду, что вам нужно убедиться, что ваш код не продолжает выполняться после вызова exitManager.exit (). Когда ваш код тестируется с помощью фиктивного ExitManager, код фактически не завершится после вызова exitManager.exit.
Joman68
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
}


РЕДАКТИРОВАТЬ: Альтернативный тест (с использованием новейшего 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)");
}
Рожерио
источник
2
Причина отклонения: проблема с этим решением состоит в том, что если System.exit не является последней строкой в ​​коде (т. Е. Внутри условия if), код продолжит выполняться.
Л. Холанда
@LeoHolanda Добавлена ​​версия теста, которая запрещает запуск кода после exitвызова (не то, чтобы это действительно было проблемой, IMO).
Rogério
Может быть уместнее заменить последний System.out.println () на оператор assert?
user515655
«@Mocked (« выход »)», похоже, больше не работает с JMockit 1.43: «Значение атрибута не определено для типа аннотации Mocked» Документы: «Существует три различных аннотации насмешки, которые мы можем использовать при объявлении полей макетов и параметры: @Mocked, который будет проверять все методы и конструкторы на всех существующих и будущих экземплярах проверяемого класса (на время тестов, использующих его); " jmockit.github.io/tutorial/Mocking.html#mocked
Торстен
Вы действительно попробовали свое предложение? Я получаю: «java.lang.IllegalArgumentException: класс java.lang.System не является mockable» Runtimeможет быть поддельным , но не останавливается, System.exitпо крайней мере, в моем сценарии запуска тестов в Gradle.
Торстен Шенинг,
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 (либо методом public foo, либо модульным тестом), но основной класс по-прежнему жестко задает реализацию Runnable по умолчанию.
Скотт Бейл
6

Создайте фиктивный класс, который обёртывает System.exit ()

Я согласен с EricSchaefer . Но если вы используете хороший Mocking Framework, такой как Mockito, достаточно простого конкретного класса, нет необходимости в интерфейсе и двух реализациях.

Остановка выполнения теста в System.exit ()

Проблема:

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

Дразнящий Sytem.exit()не прекратит выполнение. Это плохо, если вы хотите проверить, что thing2не выполняется.

Решение:

Вы должны рефакторинг этого кода в соответствии с предложением Мартина :

// 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 (). Вместо этого используйте исключение.
Андреа Франсия
3

Беглый взгляд на API показывает, что System.exit может выдать исключение ESP. если менеджер безопасности запрещает отключение виртуальной машины. Может быть, решение будет установить такой менеджер.

flolo
источник
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), а не checkPermission с name = "exitVM". Интересно, должен ли я переопределить оба?
Крис Конвей
Имя разрешения на самом деле выглядит как exitVM (код состояния), то есть exitVM.0 - по крайней мере, в моем недавнем тесте на OSX.
Марк Деррикутт
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);
    }
}
Ursa
источник
2

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

Вчера вечером я собрал небольшой JAR-файл, используя аннотации Junit @Rule, чтобы скрыть код менеджера безопасности, а также добавить ожидания, основанные на ожидаемом коде возврата. 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
Как вы будете взаимодействовать с тестируемым классом, если он находится в отдельном процессе от модульного теста?
ДНК
1
В 99% случаев System.exit находится внутри основного метода. Следовательно, вы взаимодействуете с ними с помощью аргументов командной строки (т.е. аргументов).
Л. Холанда
1

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

Class[] stack = getClassContext();
if (stack[1] != JFrame.class && !okToExit) throw new ExitException();
super.checkExit(status);
cayhorstmann
источник
0

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


источник
8
Это не отвечает на вопрос, хотя. Что если тестируемая функция в конечном итоге является основным методом? Поэтому вызов System.exit () может быть действительным и нормальным с точки зрения дизайна. Как вы пишете тестовый пример для него?
Elie
3
Вам не нужно проверять метод main, поскольку метод main должен просто принимать любые аргументы, передавать их методу синтаксического анализатора и затем запускать приложение. Не должно быть логики в основном тестируемом методе.
Томас Оуэнс
5
@ Эли: На эти типы вопросов есть два правильных ответа. Один отвечал на поставленный вопрос, а другой спрашивал, почему вопрос был основан. Оба типа ответов дают лучшее понимание, и особенно оба вместе.
runaros