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

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

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

19 ответов

Решение

Действительно, 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 года:

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

Системные правила библиотеки имеют правило 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);
    }
}

Полное раскрытие: я автор этой библиотеки.

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

Вы на самом деле можете издеваться или заглушить 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)");
}

Один из приемов, который мы использовали в нашей кодовой базе, заключался в инкапсуляции вызова 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 не выполняется.

Решение:

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

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

Системные заглушки - 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);

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

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

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

Чтобы ответ 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);
}

Вы можете использовать java SecurityManager, чтобы предотвратить отключение текущей виртуальной машины Java виртуальным потоком. Следующий код должен делать то, что вы хотите:

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

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

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

Большинство решений будут

  • завершить тест (метод, а не весь прогон) в момент System.exit() называется
  • игнорировать уже установленный SecurityManager
  • Иногда быть довольно специфичным для тестовой среды
  • ограничить использование не более одного раза в тестовом примере

Таким образом, большинство решений не подходят для ситуаций, когда:

  • Проверка побочных эффектов проводится после обращения к System.exit()
  • Существующий менеджер безопасности является частью тестирования.
  • Используется другая среда тестирования.
  • Вы хотите иметь несколько проверок в одном тестовом примере. Это может быть строго не рекомендовано, но иногда может быть очень удобным, особенно в сочетании сassertAll(), например.

Я не был доволен ограничениями, налагаемыми существующими решениями, представленными в других ответах, и поэтому придумал что-то самостоятельно.

Следующий класс предоставляет метод assertExits(int expectedStatus, Executable executable) который утверждает, что System.exit() вызывается с указанным statusзначение, и тест может продолжаться после него. Работает так же, как JUnit 5assertThrows. Он также уважает существующего менеджера по безопасности.

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

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

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

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

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

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

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

Обычно полезный подход, который можно использовать для модульного и интеграционного тестирования, состоит в том, чтобы иметь закрытый (доступ по умолчанию) имитируемый класс бегуна пакета, который предоставляет методы run () и exit (). Эти методы могут быть переопределены тестовыми классами Mock или Fake в тестовых модулях.

Тестовый класс (JUnit или другой) предоставляет исключения, которые метод exit () может генерировать вместо System.exit ().

      package mainmocked;
class MainRunner {
    void run(final String[] args) {
        new MainMocked().run(args);    
    }
    void exit(final int status) {
        System.exit(status);
    }
}

класс с main () ниже также имеет altMain () для получения фиктивного или поддельного бегуна при модульном или интеграционном тестировании:

      package mainmocked;

public class MainMocked {
    private static MainRunner runner = new MainRunner();

    static void altMain(final String[] args, final MainRunner inRunner) {
        runner = inRunner;
        main(args);
    }

    public static void main(String[] args) {
        try {
          runner.run(args);
        } catch (Throwable ex) {
            // Log("error: ", ex);
            runner.exit(1);
        }
        runner.exit(0);
    } // main


    public void run(String[] args) {
        // do things ...
    }
} // class

Простой макет (с Mockito) будет:

      @Test
public void testAltMain() {
    String[] args0 = {};
    MainRunner mockRunner = mock(MainRunner.class);
    MainMocked.altMain(args0, mockRunner);

    verify(mockRunner).run(args0);
    verify(mockRunner).exit(0);
  }

Более сложный тестовый класс будет использовать Fake, в котором run () может делать что угодно, и класс Exception для замены System.exit ():

      private class FakeRunnerRuns extends MainRunner {
    @Override
    void run(String[] args){
        new MainMocked().run(args);
    }
    @Override
    void exit(final int status) {
        if (status == 0) {
            throw new MyMockExitExceptionOK("exit(0) success");
        }
        else {
            throw new MyMockExitExceptionFail("Unexpected Exception");
        } // ok
    } // exit
} // class

Другой способ заключается в том, чтобы ввести дополнительный код в места (надеюсь, небольшое количество), где логика выполняет System.exit(). Этот дополнительный код затем избегает выполнения System.exit(), когда логика вызывается как часть модульного теста. Например, определите частную константу пакета, такую ​​как TEST_MODE, которая обычно имеет значение false. Затем задайте для кода модульного теста значение true и добавьте логику к тестируемому коду, чтобы проверить этот случай и обойти вызов System.exit, а вместо этого выбросить исключение, которое может перехватить логика модульного теста. Кстати, в 2021 году при использовании чего-то вроде Spotbugs эта проблема может проявиться в неясной ошибке «java.io.IOException: существующее соединение было принудительно закрыто удаленным хостом».

Другие вопросы по тегам