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: существующее соединение было принудительно закрыто удаленным хостом».