Java: come testare metodi che chiamano System.exit ()?

Ho alcuni metodi che dovrebbero chiamare System.exit() su determinati input. Sfortunatamente, testare questi casi fa terminare JUnit! Non sembra essere utile mettere le chiamate al metodo in una nuova discussione, poiché System.exit() termina la JVM, non solo il thread corrente. Ci sono schemi comuni per affrontare questo? Ad esempio, posso sostituire uno stub per System.exit() ?

[EDIT] La class in questione è in realtà uno strumento da riga di comando che sto tentando di testare all’interno di JUnit. Forse JUnit non è semplicemente lo strumento giusto per il lavoro? Sono graditi suggerimenti per strumenti di test di regressione complementari (preferibilmente qualcosa che si integri bene con JUnit ed EclEmma).

Infatti, Derkeiler.com suggerisce:

  • Perché System.exit() ?

Invece di terminare con System.exit (anyValue), perché non lanciare un’eccezione non controllata? Nell’uso normale andrà alla deriva fino al catcher dell’ultimo JDM e chiuderà il copione (a meno che non decidiate di prenderlo da qualche parte lungo la strada, che potrebbe essere utile un giorno).

Nello scenario JUnit verrà catturato dal framework JUnit, che segnalerà che tale e quel test sono falliti e si spostano agevolmente lungo il successivo.

  • Impedire a System.exit() di uscire effettivamente da JVM:

Prova a modificare il TestCase per l’esecuzione con un gestore della sicurezza che impedisce di chiamare System.exit, quindi cattura 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); } } } 

Aggiornamento dicembre 2012:

Propone nei commenti utilizzando le regole di sistema , una raccolta di regole di JUnit (4.9+) per testare il codice che utilizza java.lang.System .
Questo è stato inizialmente menzionato da Stefan Birkner nella sua risposta nel dicembre 2011.

 System.exit(…) 

Utilizzare la regola ExpectedSystemExit per verificare che System.exit(…) venga chiamato.
Puoi anche verificare lo stato di uscita.

Per esempio:

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

La libreria Regole di sistema ha una regola JUnit chiamata ExpectedSystemExit. Con questa regola puoi testare il codice, che chiama 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); } } 

Full disclosure: sono l’autore di quella libreria.

In realtà puoi prendere in giro o estirpare il metodo System.exit , in un test JUnit.

Ad esempio, usando JMockit puoi scrivere (ci sono anche altri modi):

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

EDIT: test alternativo (utilizzando l’ultima API JMockit) che non consente l’esecuzione di alcun codice dopo una chiamata 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)"); } 

Che ne dici di iniettare un “ExitManager” in questo metodo:

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

Il codice di produzione utilizza ExitManagerImpl e il codice di test utilizza ExitManagerMock e può controllare se è stato chiamato exit () e con quale codice di uscita.

Un trucco che usavamo nel nostro codice base era di avere la chiamata a System.exit () essere incapsulata in un impl Runnable, che il metodo in questione usava di default. Al test unitario, impostiamo un falso Runnable diverso. Qualcosa come questo:

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

… e il metodo di test JUnit …

 public void testFoo(){ final AtomicBoolean actionWasCalled = new AtomicBoolean(false); fooObject.foo(new Runnable(){ public void run(){ actionWasCalled.set(true); } }); assertTrue(actionWasCalled.get()); } 

Crea una class mock-able che avvolge System.exit ()

Sono d’accordo con EricSchaefer . Ma se si utilizza una buona struttura di derisione come Mockito è sufficiente una semplice class concreta, non c’è bisogno di un’interfaccia e di due implementazioni.

Interruzione dell’esecuzione del test su System.exit ()

Problema:

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

Un Sytem.exit() deriso non terminerà l’esecuzione. Questo è male se vuoi testare che thing2 non viene eseguito.

Soluzione:

Devi refactoring questo codice come suggerito da martin :

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

E fai System.exit(status) nella funzione di chiamata. Questo ti obbliga ad avere tutti i tuoi System.exit() in un posto vicino a main() . Questo è più pulito rispetto a System.exit() profondità nella logica.

Codice

Wrapper:

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

Principale:

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

Test:

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

Mi piacciono alcune delle risposte già fornite ma volevo dimostrare una tecnica diversa che è spesso utile quando si ottiene il codice legacy sotto test. Dato il codice come:

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

È ansible eseguire un refactoring sicuro per creare un metodo che avvolge la chiamata System.exit:

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

Quindi puoi creare un falso per il tuo test che esclude l'uscita:

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

Questa è una tecnica generica per sostituire il comportamento per i casi di test, e io la uso sempre durante il refactoring del codice legacy. Di solito non dove ho intenzione di lasciare qualcosa, ma un passaggio intermedio per ottenere il codice esistente sotto test.

Una rapida occhiata all’api, mostra che System.exit può lanciare un’eccezione esp. se un responsabile della sicurezza vieta l’arresto del VM. Forse una soluzione sarebbe quella di installare un gestore del genere.

È ansible utilizzare java SecurityManager per impedire che il thread corrente interrompa la VM Java. Il seguente codice dovrebbe fare ciò che vuoi:

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

Per la risposta di VonC per l’esecuzione su JUnit 4, ho modificato il codice come segue

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

Esistono ambienti in cui il codice di uscita restituito viene utilizzato dal programma chiamante (come ERRORLEVEL in MS Batch). Abbiamo test sui metodi principali che fanno questo nel nostro codice e il nostro approccio è stato quello di utilizzare un override di SecurityManager simile a quello utilizzato in altri test qui.

Ieri sera ho messo insieme una piccola JAR usando le annotazioni Junit @Rule per hide il codice del gestore della sicurezza, oltre ad aggiungere aspettative basate sul codice di ritorno previsto. http://code.google.com/p/junitsystemrules/

È ansible verificare System.exit (..) sostituendo l’istanza di runtime. Ad esempio 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); } } 

Chiamare System.exit () è una ctriggers pratica, a meno che non sia fatto all’interno di un main (). Questi metodi dovrebbero generare un’eccezione che, in definitiva, viene catturata dal tuo main (), che quindi chiama System.exit con il codice appropriato.

Utilizzare Runtime.exec(String command) per avviare JVM in un processo separato.

C’è un problema minore con la soluzione SecurityManager . Alcuni metodi, come JFrame.exitOnClose , chiamano anche SecurityManager.checkExit . Nella mia applicazione, non volevo che quella chiamata fallisse, quindi l’ho usata

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