Java: Як перевірити методи, які викликають System.exit ()?


198

У мене є кілька методів, які потребують System.exit()певних входів. На жаль, тестування цих випадків призводить до припинення роботи JUnit! Введення викликів методу в нову тему, здається, не допоможе, оскільки System.exit()припиняє JVM, а не лише поточну нитку. Чи є загальні закономірності для вирішення цього питання? Наприклад, чи можу я замінити заглушку System.exit()?

[EDIT] Клас, про який йде мова, насправді є інструментом командного рядка, який я намагаюся перевірити в JUnit. Може бути, JUnit просто не є правильним інструментом для роботи? Пропозиції щодо додаткових інструментів тестування регресії вітаються (бажано, що добре поєднується з JUnit та EclEmma).


2
Мені цікаво, чому функція коли-небудь викликає System.exit () ...
Томас Оуенс

2
Якщо ви викликаєте функцію, яка закриває програму. Наприклад, якщо користувач намагається виконати завдання, він не має права виконувати більше x разів поспіль, ви вимушуєте їх виходити з програми.
Елі

5
Я все ще думаю, що в цьому випадку повинен бути приємніший спосіб повернення з програми, а не System.exit ().
Томас Оуенс

27
Якщо ви тестуєте main (), то має сенс викликати System.exit (). У нас є вимога, що при помилці пакетний процес повинен вийти з 1, а при успішному виході
Matthew Farwell

5
Я не згоден з усіма, хто каже, що System.exit()це погано - ваша програма повинна швидко провалюватися. Якщо викинути виняток, це просто подовжує програму в недійсному стані в тих ситуаціях, коли розробник хоче вийти з-за помилкових помилок.
Шрідхар Сарнобат

Відповіді:


216

Дійсно, Derkeiler.com пропонує:

  • Чому System.exit()?

Замість того, щоб припиняти використання System.exit (незалежно від Valalue), чому б не кинути неперевірений виняток? При звичайному використанні він винесе весь шлях до останнього виловника JVM і закриє ваш скрипт (якщо ви не вирішите зловити його десь на шляху, що може бути корисним колись).

У сценарії JUnit він потрапить у рамку JUnit, яка повідомить, що такий і такий тест виявився невдалим і плавно рухається до наступного.

  • Запобігайте System.exit()фактичному виходу з СВМ:

Спробуйте змінити 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);
    }
}

4
Перша відповідь не сподобалась, але друга досить класна - я не поспішав з менеджерами з безпеки та припустив, що вони складніші за це. Однак як ви протестуєте менеджер безпеки / механізм тестування.
Білл К

6
Переконайтесь, що знос виконаний належним чином, інакше ваш тест не вдасться в бігуні, такому як Eclipse, оскільки програма JUnit не може вийти! :)
MetroidFan2002

6
Мені не подобається рішення за допомогою менеджера безпеки. Мені здається, що хак я просто перевірив.
Ніколай Рейслінг

6
Якщо ви використовуєте junit 4.7 або вище, дозвольте бібліотеці займатися захопленням ваших викликів System.exit. Системні правила - stefanbirkner.github.com/system-rules
Волю

6
"Замість того, щоб припиняти використання System.exit (незалежно від Value), чому б не кинути неперевірене виключення?" - тому що я використовую фреймворк обробки аргументів командного рядка, який викликає System.exitщоразу, коли надається недійсний аргумент командного рядка.
Адам Паркін

106

Бібліотечні правила системи мають правило 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 викликається / не викликається), BTW. Звичайно, використання 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 () і з яким кодом виходу.


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
}


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

2
Причина відмови від голосування: Проблема цього рішення полягає в тому, що якщо System.exit не є останнім рядком у коді (тобто всередині, якщо умова), код продовжить працювати.
Л. Голанда

@LeoHolanda Додана версія тесту, яка заважає виконувати код після exitвиклику (не те, що це справді була проблема, IMO).
Rogério

Може бути більш доцільним замінити останній System.out.println () на твердження про затвердження?
user515655

"@Mocked (" вихід ")", схоже, більше не працює з JMockit 1,43: "Значення атрибута не визначено для типу анотації Знущено" Документи: "Є три різні глузуючі анотації, які ми можемо використовувати при оголошенні макетних полів та параметри: @Mocked, який висміює всі методи та конструктори на всіх існуючих та майбутніх екземплярах макетованого класу (протягом тривалості тестів, що використовують його); " jmockit.github.io/tutorial/Mocking.html#mocked
Thorsten

Ви насправді спробували свою пропозицію? Я отримую: "java.lang.IllegalArgumentException: Клас java.lang.System is not movable" Runtimeможна знущатися, але не зупиняється System.exitпринаймні в моєму сценарії виконання тестів у Gradle.
Торстен Шьонінг

20

Один трюк, який ми використовували в нашій кодовій базі, полягав у тому, щоб виклик 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()); 
}

Це те, що вони називають ін'єкцією залежності?
Томас Ейл

2
Цей написаний приклад - це свого роду напівфабрикатна ін'єкція залежності - залежність передається методу foo-видимого foo (або методом public foo, або тестом одиниці), але основний клас все ще твердо кодує реалізацію за замовчуванням.
Скотт Бейл

6

Створіть макетний клас, який обгортає System.exit ()

Я згоден з Еріком Шефер . Але якщо ви використовуєте хороший глузливий фреймворк, як 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);
    }
}

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
Ця функція не зупиняє потік conrol, коли викликається вихід (). Використовуйте натомість виняток.
Андреа Франція

3

Швидкий огляд api показує, що System.exit може кинути виключення esp. якщо менеджер безпеки забороняє відключення vm. Можливо, рішенням було б встановити такого менеджера.


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 з ім'ям = "exitVM". Цікаво, чи варто перекривати обидва?
Кріс Конвей

Ім’ям дозволу насправді здається, що це вихідVM. (Код статусу), тобто 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 (..) із заміною екземпляра 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);
    }
}

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 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()допомогою налаштованих повідомлень.


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

0

Виклик System.exit () є поганою практикою, якщо це не робиться всередині main (). Ці методи повинні бути викидом, який, зрештою, потрапляє у ваш головний (), який потім викликає System.exit з відповідним кодом.


8
Але це не відповідає на питання. Що робити, якщо функція, що тестується, є в кінцевому рахунку основним методом? Отже, виклик System.exit () може бути дійсним і нормальним з точки зору дизайну. Як ви пишете тестовий випадок для цього?
Елі

3
Вам не доведеться тестувати основний метод, оскільки основний метод повинен просто взяти будь-які аргументи, передати їх методу парсера і після цього натиснути запуск програми. У головному методі, який слід перевірити, не повинно бути логіки.
Томас Оуенс

5
@Elie: У цих типах питань є дві дійсні відповіді. Один відповідав на поставлене запитання, і один запитував, чому це питання ґрунтується. Обидва типи відповідей дають краще розуміння, і особливо обидва разом.
runaros
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.