Java: Как проверить методы, которые вызывают System.exit ()? - PullRequest
169 голосов
/ 21 ноября 2008

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

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

Ответы [ 15 ]

195 голосов
/ 21 ноября 2008

Действительно, 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);
    }
}
89 голосов
/ 28 декабря 2011

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

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

31 голосов
/ 26 июля 2009

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

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

20 голосов
/ 21 ноября 2008

Одна хитрость, которую мы использовали в нашей кодовой базе, заключалась в инкапсуляции вызова 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()); 
}
5 голосов
/ 29 декабря 2011

Создайте фиктивный класс, который упаковывает 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() s в одном месте в или около main(). Это чище, чем звонить System.exit() глубоко внутри вашей логики.

Код

Wrapper:

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);
    }
}
5 голосов
/ 21 ноября 2008

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

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

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

3 голосов
/ 31 января 2010

Чтобы ответ 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 голосов
/ 21 ноября 2008

Вы можете использовать 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);
3 голосов
/ 21 ноября 2008

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...