Java: ¿Cómo probar métodos que llaman a System.exit ()?

199

Tengo algunos métodos que deberían invocar System.exit()ciertas entradas. Desafortunadamente, probar estos casos hace que JUnit termine. Poner las llamadas al método en un nuevo hilo no parece ayudar, ya queSystem.exit() termina la JVM, no solo el hilo actual. ¿Hay algún patrón común para lidiar con esto? Por ejemplo, ¿puedo sustituir un trozo System.exit()?

[EDITAR] La clase en cuestión es en realidad una herramienta de línea de comandos que estoy intentando probar dentro de JUnit. ¿Quizás JUnit simplemente no es la herramienta adecuada para el trabajo? Las sugerencias para herramientas de prueba de regresión complementarias son bienvenidas (preferiblemente algo que se integre bien con JUnit y EclEmma).

Chris Conway
fuente
2
Tengo curiosidad por saber por qué una función llamaría System.exit () ...
Thomas Owens
2
Si está llamando a una función que sale de la aplicación. Por ejemplo, si el usuario intenta realizar una tarea que no está autorizado a realizar más de x veces seguidas, lo expulsa de la aplicación.
Elie
55
Todavía creo que en ese caso, debería haber una mejor manera de regresar de la aplicación en lugar de System.exit ().
Thomas Owens
27
Si está probando main (), entonces tiene mucho sentido llamar a System.exit (). Tenemos el requisito de que, por error, un proceso por lotes salga con 1 y con éxito salga con 0.
Matthew Farwell
55
No estoy de acuerdo con todos los que dicen que System.exit()es malo: su programa debería fallar rápidamente. Lanzar una excepción simplemente prolonga una aplicación en un estado no válido en situaciones en las que un desarrollador quiere salir y dará errores espurios.
Sridhar Sarnobat

Respuestas:

217

De hecho, Derkeiler.com sugiere:

  • ¿Por qué System.exit()?

En lugar de terminar con System.exit (whateverValue), ¿por qué no lanzar una excepción no marcada? En uso normal, se desplazará hasta el último receptor de la JVM y cerrará su script (a menos que decida atraparlo en algún lugar del camino, lo que podría ser útil algún día).

En el escenario JUnit, será capturado por el marco JUnit, que informará que tal y tal prueba falló y avanzará sin problemas a la siguiente.

  • Evite System.exit()salir de la JVM:

Intente modificar el TestCase para que se ejecute con un administrador de seguridad que evite llamar a System.exit, luego capture la 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);
        }
    }
}

Actualización de diciembre de 2012:

Will propone en los comentarios usando las Reglas del sistema , una colección de reglas JUnit (4.9+) para probar el código que usa java.lang.System.
Esto fue mencionado inicialmente por Stefan Birkner en su respuesta en diciembre de 2011.

System.exit(…)

Use la ExpectedSystemExitregla para verificar que System.exit(…)se llama.
También puede verificar el estado de salida.

Por ejemplo:

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);
    }
}
VonC
fuente
44
No me gustó la primera respuesta, pero la segunda es genial: no me había metido con los gerentes de seguridad y asumí que eran más complicados que eso. Sin embargo, ¿cómo se prueba el administrador de seguridad / mecanismo de prueba?
Bill K
77
Asegúrese de que el desmontaje se ejecute correctamente, de lo contrario, su prueba fallará en un corredor como Eclipse porque la aplicación JUnit no puede salir. :)
MetroidFan2002
66
No me gusta la solución con el administrador de seguridad. Me parece un truco solo para probarlo.
Nicolai Reuschling
77
Si está utilizando junit 4.7 o superior, deje que una biblioteca se ocupe de capturar sus llamadas a System.exit. Reglas del sistema - stefanbirkner.github.com/system-rules
Se
66
"En lugar de terminar con System.exit (whateverValue), ¿por qué no lanzar una excepción no marcada?" - porque estoy usando el marco de procesamiento de argumentos de línea de comandos que llama System.exitsiempre que se proporciona un argumento de línea de comandos no válido.
Adam Parkin
108

La biblioteca System Lambda tiene un método. catchSystemExitCon esta regla puedes probar el código, que llama a System.exit (...):

public void MyTest {
    @Test
    public void systemExitWithArbitraryStatusCode() {
        SystemLambda.catchSystemExit(() -> {
            //the code under test, which calls System.exit(...);
        });
    }
}

Para Java 5 a 7, la biblioteca System Rules tiene una regla JUnit llamada ExpectedSystemExit. Con esta regla, puede probar el código, que llama a 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);
    }
}

Divulgación completa: soy el autor de ambas bibliotecas.

Stefan Birkner
fuente
3
Perfecto. Elegante. No tuve que cambiar una puntada de mi código original ni jugar con el administrador de seguridad. ¡Esta debería ser la mejor respuesta!
Se
2
Esta debería ser la mejor respuesta. En lugar de un debate interminable sobre el uso adecuado de System.exit, directo al grano. Además, es una solución flexible que se adhiere a JUnit, por lo que no es necesario reinventar la rueda o meterse con el administrador de seguridad.
L. Holanda
@LeoHolanda ¿No sufre esta solución el mismo "problema" que utilizó para justificar un voto negativo en mi respuesta? Además, ¿qué pasa con los usuarios de TestNG?
Rogério
2
@LeoHolanda Tienes razón; Al observar la implementación de la regla, ahora veo que usa un administrador de seguridad personalizado que arroja una excepción cuando se produce la llamada para la verificación de "salida del sistema" y, por lo tanto, finaliza la prueba. La prueba de ejemplo agregada en mi respuesta satisface ambos requisitos (verificación adecuada con System.exit para recibir llamadas / no llamadas), por cierto. El uso de ExpectedSystemRulees agradable, por supuesto; El problema es que requiere una biblioteca adicional de terceros que proporciona muy poco en términos de utilidad en el mundo real y es específica de JUnit.
Rogério
2
Existe exit.checkAssertionAfterwards().
Stefan Birkner el
31

¿Qué hay de inyectar un "ExitManager" en estos métodos:

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

El código de producción usa ExitManagerImpl y el código de prueba usa ExitManagerMock y puede verificar si se llamó a exit () y con qué código de salida.

EricSchaefer
fuente
1
+1 Buena solución. También es fácil de implementar si está utilizando Spring, ya que ExitManager se convierte en un componente simple. Solo tenga en cuenta que debe asegurarse de que su código no se ejecute después de la llamada exitManager.exit (). Cuando su código se está probando con un ExitManager simulado, el código en realidad no saldrá después de la llamada a exitManager.exit.
Joman68
31

En realidad, puedes burlarte o descartar el System.exitmétodo, en una prueba JUnit.

Por ejemplo, usando JMockit podría escribir (también hay otras formas):

@Test
public void mockSystemExit(@Mocked("exit") System mockSystem)
{
    // Called by code under test:
    System.exit(); // will not exit the program
}


EDITAR: Prueba alternativa (utilizando la última API de JMockit) que no permite que se ejecute ningún código después de una llamada a 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)");
}
Rogério
fuente
2
Motivo de rechazo: el problema con esta solución es que si System.exit no es la última línea del código (es decir, dentro de la condición), el código continuará ejecutándose.
L. Holanda
@LeoHolanda Agregó una versión de la prueba que evita que el código se ejecute después de la exitllamada (no es que realmente fuera un problema, IMO).
Rogério
¿Podría ser más apropiado reemplazar el último System.out.println () con una declaración de aserción?
user515655
"@Mocked (" exit ")" ya no funciona con JMockit 1.43: "El valor del atributo no está definido para el tipo de anotación Mocked" Los documentos: "Hay tres anotaciones simuladas diferentes que podemos usar al declarar campos simulados y parámetros: @Mocked, que se burlarán de todos los métodos y constructores en todas las instancias existentes y futuras de una clase simulada (durante la duración de las pruebas que lo usan); " jmockit.github.io/tutorial/Mocking.html#mocked
Thorsten Schöning
¿Realmente probaste tu sugerencia? Obtengo: "java.lang.IllegalArgumentException: clase java.lang.System no se puede burlar" Runtimese puede burlar, pero no se detieneSystem.exit al menos en mi escenario ejecutando Pruebas en Gradle.
Thorsten Schöning el
20

Un truco que utilizamos en nuestra base de código fue hacer que la llamada a System.exit () se encapsulara en un impl Runnable, que el método en cuestión usaba por defecto. Para la prueba unitaria, establecemos un simulacro de Runnable diferente. Algo como esto:

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

... y el método de prueba JUnit ...

public void testFoo(){   
  final AtomicBoolean actionWasCalled = new AtomicBoolean(false);   
  fooObject.foo(new Runnable(){
    public void run(){
      actionWasCalled.set(true);
    }   
  });   
  assertTrue(actionWasCalled.get()); 
}
Scott Bale
fuente
¿Es esto lo que llaman inyección de dependencia?
Thomas Ahle
2
Este ejemplo tal como está escrito es una especie de inyección de dependencia a medio hacer: la dependencia se pasa al método foo visible en el paquete (ya sea por el método foo público o por la prueba de la unidad), pero la clase principal todavía codifica la implementación Runnable predeterminada.
Scott Bale el
6

Cree una clase simulable que envuelva System.exit ()

Estoy de acuerdo con EricSchaefer . Pero si usa un buen marco de imitación como Mockito, una clase concreta simple es suficiente, no necesita una interfaz y dos implementaciones.

Detener la ejecución de la prueba en System.exit ()

Problema:

// do thing1
if(someCondition) {
    System.exit(1);
}
// do thing2
System.exit(0)

Una burla Sytem.exit()no terminará la ejecución. Esto es malo si quieres probar esothing2 no se ejecuta.

Solución:

Deberías refactorizar este código como lo sugiere Martin :

// do thing1
if(someCondition) {
    return 1;
}
// do thing2
return 0;

Y hacer System.exit(status)en la función de llamada. Esto te obliga a tener todas tus System.exit()direcciones en un solo lugar en o cerca main(). Esto es más limpio que llamarSystem.exit() profundamente dentro de su lógica.

Código

Envoltura:

public class SystemExit {

    public void exit(int status) {
        System.exit(status);
    }
}

Principal:

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

Prueba:

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);
    }
}
Arend v. Reinersdorff
fuente
5

Me gustan algunas de las respuestas ya dadas, pero quería demostrar una técnica diferente que a menudo es útil cuando se pone a prueba el código heredado. Código dado como:

public class Foo {
  public void bar(int i) {
    if (i < 0) {
      System.exit(i);
    }
  }
}

Puede realizar una refactorización segura para crear un método que envuelva la llamada System.exit:

public class Foo {
  public void bar(int i) {
    if (i < 0) {
      exit(i);
    }
  }

  void exit(int i) {
    System.exit(i);
  }
}

Luego puede crear una falsificación para su prueba que anule la salida:

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

Esta es una técnica genérica para sustituir el comportamiento de los casos de prueba, y lo uso todo el tiempo al refactorizar el código heredado. Por lo general, no es donde voy a dejar las cosas, sino un paso intermedio para probar el código existente.

Jeffrey Fredrick
fuente
2
Estas técnicas no detienen el flujo de control cuando se llama a la salida (). Use una excepción en su lugar.
Andrea Francia
3

Un vistazo rápido a la API muestra que System.exit puede lanzar una excepción especialmente. si un administrador de seguridad prohíbe el cierre de la máquina virtual. Tal vez una solución sería instalar dicho administrador.

flolo
fuente
3

Puede usar Java SecurityManager para evitar que el subproceso actual apague la máquina virtual Java. El siguiente código debe hacer lo que quieras:

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);
Marc Novakowski
fuente
Hm. Los documentos de System.exit dicen específicamente que se llamará checkExit (int), no checkPermission con name = "exitVM". Me pregunto si debería anular ambos.
Chris Conway
El nombre del permiso en realidad parece ser exitVM. (Código de estado), es decir, exitVM.0, al menos en mi prueba reciente en OSX.
Mark Derricutt
3

Para que la respuesta de VonC se ejecute en JUnit 4, modifiqué el código de la siguiente manera

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);
}
Jeow Li Huan
fuente
3

Puede probar System.exit (..) reemplazando la instancia de Runtime. Por ejemplo, con 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);
    }
}
ursa
fuente
2

Hay entornos en los que el programa de llamada utiliza el código de salida devuelto (como ERRORLEVEL en MS Batch). Tenemos pruebas en torno a los principales métodos que hacen esto en nuestro código, y nuestro enfoque ha sido utilizar una anulación de SecurityManager similar a la utilizada en otras pruebas aquí.

Anoche armé un pequeño JAR usando las anotaciones de Junit @Rule para ocultar el código del administrador de seguridad, así como para agregar expectativas basadas en el código de retorno esperado. http://code.google.com/p/junitsystemrules/

Dan Watt
fuente
2

La mayoría de las soluciones lo harán

  • terminar la prueba (método, no toda la ejecución) System.exit()se llama el momento
  • ignorar un ya instalado SecurityManager
  • A veces sea bastante específico para un marco de prueba
  • restringir para ser utilizado al máximo una vez por caso de prueba

Por lo tanto, la mayoría de las soluciones no son adecuadas para situaciones en las que:

  • La verificación de los efectos secundarios se realizará después de la llamada a System.exit()
  • Un administrador de seguridad existente es parte de las pruebas.
  • Se utiliza un marco de prueba diferente.
  • Desea tener múltiples verificaciones en un solo caso de prueba. Esto puede no ser estrictamente recomendado, pero a veces puede ser muy conveniente, especialmente en combinación con assertAll(), por ejemplo.

No estaba contento con las restricciones impuestas por las soluciones existentes presentadas en las otras respuestas y, por lo tanto, se me ocurrió algo por mi cuenta.

La siguiente clase proporciona un método assertExits(int expectedStatus, Executable executable)que afirma que System.exit()se llama con un statusvalor especificado , y la prueba puede continuar después. Funciona de la misma manera que JUnit 5 assertThrows. También respeta a un administrador de seguridad existente.

Hay un problema restante: cuando el código bajo prueba instala un nuevo administrador de seguridad que reemplaza completamente al administrador de seguridad establecido por la prueba. Todas las otras SecurityManagersoluciones basadas en mí que conozco sufren el mismo problema.

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

Puedes usar la clase así:

    @Test
    void example() {
        assertExits(0, () -> System.exit(0)); // succeeds
        assertExits(1, () -> System.exit(1)); // succeeds
        assertExits(2, () -> System.exit(1)); // fails
    }

El código se puede portar fácilmente a JUnit 4, TestNG o cualquier otro marco, si es necesario. El único elemento específico del marco está fallando la prueba. Esto se puede cambiar fácilmente a algo independiente del marco (que no sea un Junit 4 Rule

Hay margen de mejora, por ejemplo, sobrecarga de assertExits()mensajes personalizables.

Christian Hujer
fuente
1

Use Runtime.exec(String command)para iniciar JVM en un proceso separado.

Alexei
fuente
3
¿Cómo va a interactuar con la clase bajo prueba, si está en un proceso separado de la prueba unitaria?
ADN
1
En el 99% de los casos, System.exit está dentro de un método principal. Por lo tanto, interactúa con ellos mediante argumentos de línea de comandos (es decir, args).
L. Holanda
1

Hay un pequeño problema con la SecurityManagersolución. Algunos métodos, como JFrame.exitOnClose, también llaman SecurityManager.checkExit. En mi solicitud, no quería que la llamada fallara, así que usé

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

Llamar a System.exit () es una mala práctica, a menos que se haga dentro de un main (). Estos métodos deberían arrojar una excepción que, en última instancia, es captada por su main (), que luego llama a System.exit con el código apropiado.


fuente
8
Sin embargo, eso no responde la pregunta. ¿Qué sucede si la función que se prueba ES, en última instancia, el método principal? Entonces llamar a System.exit () podría ser válido y correcto desde una perspectiva de diseño. ¿Cómo se escribe un caso de prueba para ello?
Elie
3
No debería tener que probar el método principal, ya que el método principal solo debe tomar cualquier argumento, pasarlo a un método de analizador y luego iniciar la aplicación. No debe haber lógica en el método principal a probar.
Thomas Owens
55
@Elie: En este tipo de preguntas hay dos respuestas válidas. Uno respondiendo a la pregunta planteada y otro preguntando por qué se basó la pregunta. Ambos tipos de respuestas dan una mejor comprensión, y especialmente ambas juntas.
runaros