java - proper - unit testing good practices
Java: ¿Cómo probar los métodos que llaman a System.exit()? (15)
Crea una clase simulada que envuelve System.exit ()
Estoy de acuerdo con EricSchaefer . Pero si usa un buen marco burlón como Mockito una simple clase concreta es suficiente, sin necesidad de una interfaz y dos implementaciones.
Detener ejecución de prueba en System.exit ()
Problema:
// do thing1
if(someCondition) {
System.exit(1);
}
// do thing2
System.exit(0)
Un Sytem.exit()
no terminará la ejecución. Esto es malo si quieres probar que thing2
no se ejecuta.
Solución:
Deberías refactorizar este código según lo sugerido por martin :
// do thing1
if(someCondition) {
return 1;
}
// do thing2
return 0;
Y haga System.exit(status)
en la función de llamada. Esto te obliga a tener todos tus System.exit()
s en un lugar en o cerca de main()
. Esto es más limpio que llamar a System.exit()
dentro de tu 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);
}
}
Tengo algunos métodos que deben llamar a System.exit()
en ciertas entradas. Desafortunadamente, probar estos casos hace que JUnit finalice. Poner las llamadas de método en un nuevo subproceso no parece ayudar, ya que System.exit()
termina la JVM, no solo el subproceso actual. ¿Hay algún patrón común para lidiar con esto? Por ejemplo, ¿puedo sustituir un stub por 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. Tal vez JUnit simplemente no es la herramienta adecuada para el trabajo? Sugerencias para herramientas de prueba de regresión complementarias son bienvenidas (preferiblemente algo que se integre bien con JUnit y EclEmma).
¿Qué tal si inyectas un "ExitManager" en este método?
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.
De hecho, Derkeiler.com sugiere:
- Por qué
System.exit()
?
En lugar de terminar con System.exit (whateverValue), ¿por qué no lanzar una excepción sin marcar? Con un uso normal, se desplazará hasta el último receptor de la JVM y cerrará el script (a menos que decidas atraparlo en algún punto 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 se moverá sin problemas a la siguiente.
- Evite que
System.exit()
salga realmente de la JVM:
Intente modificar TestCase para que se ejecute con un administrador de seguridad que evite llamar a System.exit, luego capture 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 el uso de Reglas del sistema , una colección de reglas JUnit (4.9+) para probar 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 regla
ExpectedSystemExit
para verificar que seSystem.exit(…)
.
También puedes 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);
}
}
En realidad, puede System.exit
o System.exit
método System.exit
, en una prueba JUnit.
Por ejemplo, usando JMockit puedes 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 la ejecución de 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)");
}
Existen entornos en los que el programa de llamada usa el código de salida devuelto (como ERRORLEVEL en MS Batch). Tenemos pruebas de los principales métodos que hacen esto en nuestro código, y nuestro enfoque ha sido utilizar una anulación similar de SecurityManager como se usa en otras pruebas aquí.
Anoche armé un JAR pequeño usando las anotaciones de Junit @Rule para ocultar el código del administrador de seguridad, así como agregar expectativas basadas en el código de retorno esperado. http://code.google.com/p/junitsystemrules/
Hay un problema menor con la solución SecurityManager
. Algunos métodos, como JFrame.exitOnClose
, también llaman a SecurityManager.checkExit
. En mi aplicación, 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);
La biblioteca Reglas del sistema tiene una regla JUnit llamada ExpectedSystemExit. Con esta regla, puedes probar el código que llama 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 esa biblioteca.
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.
Me gustan algunas de las respuestas ya dadas, pero quería demostrar una técnica diferente que a menudo es útil cuando se prueba el código heredado. Código dado como:
public class Foo {
public void bar(int i) {
if (i < 0) {
System.exit(i);
}
}
}
Puede hacer una refactorización segura para crear un método que envuelva la llamada a 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 un falso 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 comportamientos para casos de prueba, y la utilizo todo el tiempo cuando refacto el código heredado. Usualmente no es donde voy a dejar algo, sino como un paso intermedio para probar el código existente.
Para que la respuesta de VonC se ejecute en JUnit 4, he modificado 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);
}
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);
}
}
Puede usar el SecurityManager de Java para evitar que el hilo actual apague la máquina virtual de Java. El siguiente código debería 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);
Un truco que utilizamos en nuestra base de código fue hacer que la llamada a System.exit () se encapsulara en una ejecución Runnable, que el método en cuestión utilizaba por defecto. Para probar la unidad, establecemos un simulacro diferente Runnable. 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());
}
Una rápida mirada a la API, muestra que System.exit puede arrojar una excepción esp. si un administrador de seguridad prohíbe el cierre de la vm. Tal vez una solución sería instalar dicho administrador.
Use Runtime.exec(String command)
para iniciar JVM en un proceso separado.