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


199

Ho alcuni metodi che dovrebbero chiamare System.exit()determinati input. Sfortunatamente, testare questi casi fa terminare JUnit! Inserire le chiamate di metodo in un nuovo thread non sembra aiutare, da alloraSystem.exit() termina la JVM, non solo il thread corrente. Ci sono schemi comuni per affrontarlo? Ad esempio, posso sostituire uno stub per System.exit()?

[EDIT] La classe 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? Suggerimenti per strumenti di test di regressione complementari sono ben accetti (preferibilmente qualcosa che si integra bene con JUnit ed EclEmma).


2
Sono curioso di sapere perché una funzione avrebbe mai chiamato System.exit () ...
Thomas Owens,

2
Se stai chiamando una funzione che esce dall'applicazione. Ad esempio, se l'utente tenta di eseguire un'attività non è autorizzato a eseguire più di x volte di seguito, le forzate a uscire dall'applicazione.
Elie,

5
Penso ancora che in quel caso, ci dovrebbe essere un modo migliore per tornare dall'applicazione piuttosto che System.exit ().
Thomas Owens,

27
Se stai testando main (), ha perfettamente senso chiamare System.exit (). Abbiamo un requisito che, in caso di errore, un processo batch debba terminare con 1 e in caso di esito positivo con 0.
Matthew Farwell,

5
Non sono d'accordo con tutti coloro che dicono che System.exit()è un male: il tuo programma dovrebbe fallire rapidamente. Generare un'eccezione prolunga semplicemente un'applicazione in uno stato non valido in situazioni in cui uno sviluppatore desidera uscire e genererà errori spuri.
Sridhar Sarnobat,

Risposte:


217

Infatti, Derkeiler.com suggerisce:

  • Perché System.exit()?

Invece di terminare con System.exit (whateverValue), perché non generare un'eccezione non selezionata? Nell'uso normale si sposterà fino all'ultimo acchiappatore della JVM e chiuderà il tuo script (a meno che tu non decida di prenderlo da qualche parte lungo il percorso, che potrebbe essere utile un giorno).

Nello scenario JUnit verrà catturato dal framework JUnit, che riporterà che tale test fallito e si sposta senza intoppi al successivo.

  • Impedisci System.exit()di uscire effettivamente dalla JVM:

Prova a modificare TestCase per l'esecuzione con un gestore della sicurezza che impedisce di chiamare System.exit, quindi intercetta 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:

Will propone nei commenti l' utilizzo di Regole di sistema , una raccolta di regole JUnit (4.9+) per testare il codice che utilizza java.lang.System.
Lo ha menzionato inizialmente Stefan Birkner nella sua risposta nel dicembre 2011.

System.exit(…)

Utilizzare la ExpectedSystemExitregola 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);
    }
}

4
La prima risposta non mi è piaciuta, ma la seconda è piuttosto interessante: non avevo fatto casini con i responsabili della sicurezza e ho pensato che fossero più complicati di così. Tuttavia, come testare il gestore della sicurezza / meccanismo di test.
Bill K,

7
Assicurati che lo smontaggio sia eseguito correttamente, altrimenti il ​​test fallirà in un corridore come Eclipse perché l'applicazione JUnit non può uscire! :)
MetroidFan2002,

6
Non mi piace la soluzione utilizzando il gestore della sicurezza. Mi sembra un trucco solo per testarlo.
Nicolai Reuschling,

7
Se stai usando junit 4.7 o successive, lascia che una libreria si occupi di catturare le tue chiamate System.exit. Regole di sistema - stefanbirkner.github.com/system-rules
Sarà il

6
"Invece di terminare con System.exit (whateverValue), perché non generare un'eccezione non selezionata?" - perché sto usando il framework di elaborazione degli argomenti della riga di comando che chiama System.exitogni volta che viene fornito un argomento della riga di comando non valido.
Adam Parkin,

108

La libreria System Lambda ha un metodo catchSystemExit. Con questa regola puoi testare il codice, che chiama System.exit (...):

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

Per Java da 5 a 7 la libreria Regole di sistema ha una regola JUnit chiamata ExpectedSystemExit. Con questa regola sei in grado di 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);
    }
}

Divulgazione completa: sono l'autore di entrambe le biblioteche.


3
Perfetto. Elegante. Non è stato necessario modificare un punto del mio codice originale o giocare con il responsabile della sicurezza. Questa dovrebbe essere la risposta migliore!
Sarà il

2
Questa dovrebbe essere la risposta migliore. Invece di un dibattito senza fine sull'uso corretto di System.exit, direttamente al punto. Inoltre, una soluzione flessibile in aderenza a JUnit stessa, quindi non abbiamo bisogno di reinventare la ruota o fare confusione con il responsabile della sicurezza.
L. Holanda,

@LeoHolanda Questa soluzione non soffre dello stesso "problema" che hai usato per giustificare un voto negativo sulla mia risposta? Inoltre, che dire degli utenti TestNG?
Rogério,

2
@LeoHolanda Hai ragione; osservando l'implementazione della regola, ora vedo che utilizza un gestore della sicurezza personalizzato che genera un'eccezione quando si verifica la chiamata per il controllo "uscita sistema", quindi termina il test. Il test di esempio aggiunto nella mia risposta soddisfa entrambi i requisiti (corretta verifica con System.exit che viene chiamato / non chiamato), BTW. L'uso di ExpectedSystemRuleè bello, ovviamente; il problema è che richiede una libreria extra di terze parti che fornisce pochissimo in termini di utilità nel mondo reale ed è specifica di JUnit.
Rogério,

2
C'è exit.checkAssertionAfterwards().
Stefan Birkner,

31

Che ne dici di iniettare un "ExitManager" in questi metodi:

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ò verificare se è stato chiamato exit () e con quale codice di uscita.


1
+1 Bella soluzione. Inoltre, è facile da implementare se si utilizza Spring poiché ExitManager diventa un semplice componente. Basta essere consapevoli del fatto che è necessario assicurarsi che il codice non continui a essere eseguito dopo la chiamata exitManager.exit (). Quando il tuo codice viene testato con un ExitManager falso, il codice non verrà effettivamente chiuso dopo la chiamata a exitManager.exit.
Joman68,

31

In realtà è possibile prendere in giro o stub il System.exitmetodo, in un test JUnit.

Ad esempio, usando JMockit potresti 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)");
}

2
Vota il motivo: il problema con questa soluzione è che se System.exit non è l'ultima riga del codice (ovvero all'interno di if condition), il codice continuerà a essere eseguito.
L. Holanda,

@LeoHolanda Aggiunta una versione del test che impedisce l'esecuzione del codice dopo la exitchiamata (non che si trattasse davvero di un problema, IMO).
Rogério,

Potrebbe essere più appropriato sostituire l'ultimo System.out.println () con un'istruzione assert?
user515655

"@Mocked (" exit ")" non sembra funzionare più con JMockit 1.43: "Il valore dell'attributo non è definito per il tipo di annotazione Deriso" I documenti: "Esistono tre diverse annotazioni di derisione che possiamo usare quando dichiariamo campi di simulazione e parametri: @Mocked, che deriderà tutti i metodi e i costruttori su tutte le istanze esistenti e future di una classe derisa (per la durata dei test che la utilizzano); " jmockit.github.io/tutorial/Mocking.html#mocked
Thorsten Schöning

Hai davvero provato il tuo suggerimento? Ottengo: "java.lang.IllegalArgumentException: Class java.lang.System non è beffardo" Runtimepuò essere deriso, ma non si ferma System.exitalmeno nel mio scenario con Test in Gradle.
Thorsten Schöning,

20

Un trucco che abbiamo usato nella nostra base di codice era che la chiamata a System.exit () fosse incapsulata in un impl Runnable, che il metodo in questione usava di default. Per test unitario, abbiamo impostato un 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()); 
}

È questo ciò che chiamano iniezione di dipendenza?
Thomas Ahle,

2
Questo esempio, come scritto, è una sorta di iniezione di dipendenza semi-cotta: la dipendenza viene passata al metodo foo visibile al pacchetto (tramite il metodo foo pubblico o il test unitario), ma la classe principale codifica ancora l'implementazione Runnable predefinita.
Scott Bale,

6

Crea una classe fittizia che avvolge System.exit ()

Sono d'accordo con EricSchaefer . Ma se usi un buon framework beffardo come Mockito , è sufficiente una semplice classe concreta, senza bisogno di un'interfaccia e due implementazioni.

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

Problema:

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

Una derisione Sytem.exit()non termina l'esecuzione. Questo è male se vuoi provarlothing2 non venga eseguito.

Soluzione:

Dovresti refactificare questo codice come suggerito da Martin :

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

E fare System.exit(status)nella funzione di chiamata. Questo ti costringe ad avere tutti i tuoi System.exit()in un posto dentro o vicino main(). Questo è più pulito che chiamare in System.exit()profondità nella tua 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);
    }
}

5

Mi piacciono alcune delle risposte già fornite, ma volevo dimostrare una tecnica diversa che è spesso utile quando si verifica il codice legacy. Codice dato come:

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

È possibile eseguire un refactoring sicuro per creare un metodo che racchiuda 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 è possibile creare un falso per il test che ignora 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 ai casi di test e lo uso sempre per il refactoring del codice legacy. Di solito non è dove lascerò qualcosa, ma un passaggio intermedio per mettere alla prova il codice esistente.


2
Questa tecnica non arresta il flusso di controllo quando è stato chiamato exit (). Utilizzare invece un'eccezione.
Andrea Francia,

3

Un rapido sguardo all'API mostra che System.exit può generare un'eccezione esp. se un responsabile della sicurezza vieta l'arresto di VM. Forse una soluzione sarebbe installare un tale gestore.


3

È possibile utilizzare java SecurityManager per impedire al thread corrente di arrestare la VM Java. Il seguente codice dovrebbe fare quello 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);

Hm. I documenti System.exit dicono specificamente che verrà chiamato checkExit (int), non checkPermission con name = "exitVM". Mi chiedo se dovrei ignorare entrambi?
Chris Conway,

Il nome del permesso in realtà sembra essere exitVM. (Statuscode), cioè exitVM.0 - almeno nel mio recente test su OSX.
Mark Derricutt,

3

Per far funzionare la risposta di VonC 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);
}

3

È possibile testare 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);
    }
}

2

Esistono ambienti in cui il programma di chiamata utilizza il codice di uscita restituito (come ERRORLEVEL in MS Batch). Abbiamo test sui metodi principali che eseguono questa operazione nel nostro codice e il nostro approccio è stato quello di utilizzare un override SecurityManager simile a quello utilizzato in altri test qui.

Ieri sera ho messo insieme un piccolo JAR usando le annotazioni di Junit @Rule per nascondere il codice del gestore della sicurezza, nonché aggiungere aspettative in base al codice di ritorno previsto. http://code.google.com/p/junitsystemrules/


2

La maggior parte delle soluzioni lo faranno

  • termina il test (metodo, non l'intera corsa) il momento System.exit() viene chiamato
  • ignora un già installato SecurityManager
  • A volte è abbastanza specifico per un framework di test
  • limitare l'uso massimo una volta per caso di test

Pertanto, la maggior parte delle soluzioni non è adatta a situazioni in cui:

  • La verifica degli effetti collaterali deve essere eseguita dopo la chiamata a System.exit()
  • Un gestore della sicurezza esistente fa parte del test.
  • Viene utilizzato un diverso framework di test.
  • Desideri avere più verifiche in un singolo caso di test. Questo può essere rigorosamente sconsigliato, ma a volte può essere molto conveniente, specialmente in combinazione con assertAll(), per esempio.

Non ero contento delle restrizioni imposte dalle soluzioni esistenti presentate nelle altre risposte, e quindi ho escogitato qualcosa da solo.

La seguente classe fornisce un metodo assertExits(int expectedStatus, Executable executable)che afferma di System.exit()essere chiamato con un statusvalore specificato e il test può continuare dopo di esso. Funziona allo stesso modo di JUnit 5assertThrows . Rispetta anche un gestore della sicurezza esistente.

Rimane un problema: quando il codice in prova installa un nuovo gestore della sicurezza che sostituisce completamente il gestore della sicurezza impostato dal test. Tutte le altre SecurityManagersoluzioni basate su me conosciute soffrono dello stesso 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;
        }
    }
}

Puoi usare la classe in questo modo:

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

Il codice può essere facilmente trasferito su JUnit 4, TestNG o qualsiasi altro framework, se necessario. L'unico elemento specifico del framework sta fallendo il test. Questo può essere facilmente modificato in qualcosa di indipendente dal framework (diverso da Junit 4 Rule

Vi sono margini di miglioramento, ad esempio il sovraccarico assertExits()di messaggi personalizzabili.


1

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


3
Come interagirai con la classe sotto test, se si trova in un processo separato dall'unità test?
DNA,

1
Nel 99% dei casi System.exit è all'interno di un metodo principale. Pertanto, interagisci con loro tramite argomenti della riga di comando (es. Args).
L. Holanda,

1

C'è un piccolo problema con la SecurityManagersoluzione. Alcuni metodi, come ad esempio 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);

0

Chiamare System.exit () è una cattiva pratica, a meno che non sia fatto all'interno di un main (). Questi metodi dovrebbero generare un'eccezione che, alla fine, viene catturata da main (), che quindi chiama System.exit con il codice appropriato.


8
Questo non risponde alla domanda, però. Cosa succede se la funzione testata è in definitiva il metodo principale? Quindi chiamare System.exit () potrebbe essere valido e ok dal punto di vista del design. Come si scrive un test case per questo?
Elie,

3
Non dovresti testare il metodo principale in quanto il metodo principale dovrebbe semplicemente prendere qualsiasi argomento, passarli a un metodo parser e quindi avviare l'applicazione. Non ci dovrebbe essere alcuna logica nel metodo principale da testare.
Thomas Owens,

5
@Elie: in questi tipi di domande ci sono due risposte valide. Uno ha risposto alla domanda posta e uno ha chiesto perché la domanda fosse fondata. Entrambi i tipi di risposte forniscono una migliore comprensione, e soprattutto entrambi insieme.
Runaros,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.