Java: Как проверить методы, которые вызывают System.exit()?
У меня есть несколько методов, которые должны называть System.exit()
на определенных входах. К сожалению, тестирование этих случаев заставляет JUnit прекратить работу! Помещение вызовов метода в новый поток, похоже, не помогает, поскольку System.exit()
завершает JVM, а не только текущий поток. Существуют ли общие шаблоны для решения этой проблемы? Например, могу ли я оставить заглушку для System.exit()
?
[EDIT] Этот класс - это инструмент командной строки, который я пытаюсь протестировать внутри JUnit. Может быть, JUnit - это просто не лучший инструмент для работы? Предложения по дополнительным инструментам тестирования регрессии приветствуются (желательно то, что хорошо интегрируется с JUnit и EclEmma).
Ответы
Ответ 1
Действительно, Derkeiler.com предлагает:
Вместо того, чтобы завершаться с помощью System.exit(whateverValue), почему бы не выбросить исключенное исключение? При нормальном использовании он будет дрейфовать до самого последнего ловушка JVM и закрыть ваш script вниз (если вы не решите поймать его где-то на этом пути, что может когда-нибудь быть полезным).
В сценарии JUnit он будет зависеть от структуры JUnit, которая сообщит, что такой-то тест не прошел и плавно переходил к следующему.
- Предотвратить
System.exit()
для фактического выхода из JVM:
Попробуйте модифицировать TestCase для запуска с помощью диспетчера безопасности, который запрещает вызов System.exit, а затем catch 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);
}
}
Ответ 2
В библиотеке System Rules есть правило 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
Фактически вы можете высмеивать или заглушать метод 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)");
}
Ответ 4
Как насчет ввода "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() и с каким кодом выхода.
Ответ 5
Один трюк, который мы использовали в нашей базе кода, состоял в том, чтобы вызов System.exit() был инкапсулирован в Runnable impl, который по умолчанию использовался по умолчанию. Для unit test мы устанавливаем другой макет 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());
}
Ответ 6
Создайте макет-класс, который обертывает 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()
в одном месте или рядом с main()
. Это более чистое, чем вызов System.exit()
внутри вашей логики.
код
Упаковочный:
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);
}
}
Ответ 7
Мне нравятся некоторые из уже полученных ответов, но я хотел продемонстрировать другую технику, которая часто бывает полезной при тестировании устаревшего кода. С учетом кода:
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;
}
}
Это общий метод замены поведения для тестовых случаев, и я использую его все время, когда рефакторинг устаревшего кода. Обычно я не собираюсь оставлять вещи, но это промежуточный шаг для проверки существующего кода.
Ответ 8
Быстрый просмотр api, показывает, что System.exit может вызывать исключение esp. если диспетчер безопасности запрещает завершение работы vm. Возможно, решение было бы установить такого менеджера.
Ответ 9
Вы можете использовать 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);
Ответ 10
Для ответа 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);
}
Ответ 11
Calling System.exit() - это плохая практика, если только она не используется внутри main(). Эти методы должны вызывать исключение, которое, в конечном счете, попадает на ваш основной(), который затем вызывает System.exit с соответствующим кодом.
Ответ 12
Существуют среды, в которых возвращаемый код выхода используется вызывающей программой (например, ERRORLEVEL в MS Batch). У нас есть тесты вокруг основных методов, которые делают это в нашем коде, и наш подход заключался в использовании аналогичного переопределения SecurityManager, используемого в других тестах здесь.
Вчера вечером я собрал небольшой JAR, используя аннотации Junit @Rule, чтобы скрыть код менеджера безопасности, а также добавить ожидания на основе ожидаемого кода возврата. http://code.google.com/p/junitsystemrules/
Ответ 13
Вы можете протестировать 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);
}
}
Ответ 14
Используйте Runtime.exec(String command)
для запуска JVM в отдельном процессе.
Ответ 15
Существует небольшая проблема с решением SecurityManager
. Некоторые методы, такие как JFrame.exitOnClose
, также вызывают SecurityManager.checkExit
. В моем приложении я не хотел, чтобы этот вызов терпел неудачу, поэтому я использовал
Class[] stack = getClassContext();
if (stack[1] != JFrame.class && !okToExit) throw new ExitException();
super.checkExit(status);