Java: comment tester les méthodes qui appellent System.exit ()?


199

J'ai quelques méthodes qui devraient faire appel System.exit()à certaines entrées. Malheureusement, le test de ces cas entraîne l'arrêt de JUnit! Placer les appels de méthode dans un nouveau thread ne semble pas aider, car System.exit()met fin à la JVM, pas seulement au thread actuel. Existe-t-il des schémas communs pour gérer cela? Par exemple, puis-je remplacer un talon pour System.exit()?

[EDIT] La classe en question est en fait un outil en ligne de commande que j'essaie de tester dans JUnit. Peut-être que JUnit n'est tout simplement pas le bon outil pour le travail? Les suggestions d'outils de test de régression complémentaires sont les bienvenues (de préférence quelque chose qui s'intègre bien avec JUnit et EclEmma).


2
Je suis curieux de savoir pourquoi une fonction appellera jamais System.exit () ...
Thomas Owens

2
Si vous appelez une fonction qui quitte l'application. Par exemple, si l'utilisateur essaie d'effectuer une tâche, il n'est pas autorisé à effectuer plus de x fois de suite, vous le forcez à quitter l'application.
Elie

5
Je pense toujours que dans ce cas, il devrait y avoir un meilleur moyen de revenir de l'application plutôt que System.exit ().
Thomas Owens

27
Si vous testez main (), il est parfaitement logique d'appeler System.exit (). Nous avons une exigence qu'en cas d'erreur, un processus par lots se termine avec 1 et en cas de succès, quitte avec 0.
Matthew Farwell

5
Je suis en désaccord avec tous ceux qui disent que System.exit()c'est mauvais - votre programme devrait échouer rapidement. Le fait de lever une exception prolonge simplement une application dans un état non valide dans les situations où un développeur souhaite quitter et donnera de fausses erreurs.
Sridhar Sarnobat,

Réponses:


217

En effet, Derkeiler.com propose:

  • Pourquoi System.exit()?

Au lieu de terminer avec System.exit (quelle que soit la valeur), pourquoi ne pas lever une exception non vérifiée? En utilisation normale, il dérivera jusqu'au dernier récupérateur de la JVM et fermera votre script (sauf si vous décidez de l'attraper quelque part en cours de route, ce qui pourrait être utile un jour).

Dans le scénario JUnit, il sera intercepté par le framework JUnit, qui signalera que tel ou tel test a échoué et se déplacera en douceur vers le suivant.

  • Prévenir System.exit() de quitter réellement la JVM:

Essayez de modifier le TestCase pour qu'il s'exécute avec un gestionnaire de sécurité qui empêche d'appeler System.exit, puis interceptez 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);
        }
    }
}

Mise à jour de décembre 2012:

Will propose dans les commentaires à l' aide de System Rules , une collection de règles JUnit (4.9+) pour tester le code qui utilise java.lang.System.
Cela a été initialement mentionné par Stefan Birkner dans sa réponse de décembre 2011.

System.exit(…)

Utilisez la ExpectedSystemExitrègle pour vérifier qu'elle System.exit(…)est appelée.
Vous pouvez également vérifier l'état de sortie.

Par exemple:

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
Je n'ai pas aimé la première réponse, mais la seconde est plutôt cool - je n'avais pas joué avec les responsables de la sécurité et je pensais qu'ils étaient plus compliqués que ça. Cependant, comment testez-vous le gestionnaire de sécurité / mécanisme de test.
Bill K

7
Assurez-vous que le démontage est exécuté correctement, sinon, votre test échouera dans un exécuteur tel qu'Eclipse car l'application JUnit ne peut pas quitter! :)
MetroidFan2002

6
Je n'aime pas la solution utilisant le gestionnaire de sécurité. On dirait un hack pour moi juste pour le tester.
Nicolai Reuschling

7
Si vous utilisez junit 4.7 ou supérieur, laissez une bibliothèque gérer la capture de vos appels System.exit. Règles du système - stefanbirkner.github.com/system-rules
sera

6
"Au lieu de terminer avec System.exit (quelle que soit la valeur), pourquoi ne pas lever une exception non vérifiée?" - parce que j'utilise le cadre de traitement des arguments de ligne de commande qui appelle System.exitchaque fois qu'un argument de ligne de commande non valide est fourni.
Adam Parkin

108

La bibliothèque System Lambda a une méthode. catchSystemExitAvec cette règle vous pouvez tester le code, qui appelle System.exit (...):

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

Pour Java 5 à 7, les règles système de la bibliothèque ont une règle JUnit appelée ExpectedSystemExit. Avec cette règle, vous pouvez tester le code qui appelle 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);
    }
}

Divulgation complète: je suis l'auteur des deux bibliothèques.


3
Parfait. Élégant. Je n'ai pas eu à changer un point de mon code d'origine ou à jouer avec le responsable de la sécurité. Cela devrait être la meilleure réponse!
Will

2
Cela devrait être la meilleure réponse. Au lieu d'un débat sans fin sur la bonne utilisation de System.exit, allez droit au but. De plus, une solution flexible dans le respect de JUnit lui-même, nous n'avons donc pas besoin de réinventer la roue ou de gâcher le responsable de la sécurité.
L. Holanda

@LeoHolanda Cette solution ne souffre-t-elle pas du même "problème" que vous avez utilisé pour justifier un downvote sur ma réponse? Et les utilisateurs de TestNG?
Rogério

2
@LeoHolanda Vous avez raison; en regardant l'implémentation de la règle, je vois maintenant qu'elle utilise un gestionnaire de sécurité personnalisé qui lève une exception lorsque l'appel pour la vérification de "sortie système" se produit, mettant ainsi fin au test. L'exemple de test ajouté dans ma réponse satisfait aux deux exigences (vérification correcte avec System.exit appelé / non appelé), BTW. L'utilisation de ExpectedSystemRuleest bien sûr agréable; le problème est qu'il nécessite une bibliothèque tierce supplémentaire qui fournit très peu en termes d'utilité dans le monde réel et est spécifique à JUnit.
Rogério

2
Il y en a exit.checkAssertionAfterwards().
Stefan Birkner

31

Que diriez-vous d'injecter un "ExitManager" dans ces méthodes:

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

Le code de production utilise ExitManagerImpl et le code de test utilise ExitManagerMock et peut vérifier si exit () a été appelé et avec quel code de sortie.


1
+1 Belle solution. Aussi facile à implémenter si vous utilisez Spring car ExitManager devient un simple composant. Sachez simplement que vous devez vous assurer que votre code ne continue pas à s'exécuter après l'appel exitManager.exit (). Lorsque votre code est testé avec un faux ExitManager, le code ne sortira pas réellement après l'appel à exitManager.exit.
Joman68

31

Vous pouvez réellement vous moquer ouSystem.exit méthode, dans un test JUnit.

Par exemple, en utilisant JMockit, vous pourriez écrire (il existe également d'autres moyens):

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


EDIT: Test alternatif (utilisant la dernière API JMockit) qui ne permet à aucun code de s'exécuter après un appel à 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
Raison du vote: le problème avec cette solution est que si System.exit n'est pas la dernière ligne du code (c'est-à-dire à l'intérieur si la condition), le code continuera à s'exécuter.
L. Holanda

@LeoHolanda Ajout d'une version du test qui empêche le code de s'exécuter après l' exitappel (pas que c'était vraiment un problème, OMI).
Rogério

Serait-il plus approprié de remplacer le dernier System.out.println () par une instruction assert?
user515655

"@Mocked (" exit ")" ne semble plus fonctionner avec JMockit 1.43: "La valeur d'attribut n'est pas définie pour le type d'annotation Mocked" The docs: "Il existe trois annotations de mocking différentes que nous pouvons utiliser lors de la déclaration des champs fictifs et paramètres: @Mocked, qui simulera toutes les méthodes et tous les constructeurs sur toutes les instances existantes et futures d'une classe simulée (pendant la durée des tests l'utilisant); " jmockit.github.io/tutorial/Mocking.html#mocked
Thorsten Schöning

Avez-vous réellement essayé votre suggestion? J'obtiens: "java.lang.IllegalArgumentException: la classe java.lang.System n'est pas moquable" Runtimepeut être moquée, mais ne s'arrête pas System.exitau moins dans mon scénario exécutant des tests dans Gradle.
Thorsten Schöning

20

Une astuce que nous avons utilisée dans notre base de code était d'avoir l'appel à System.exit () être encapsulé dans un implément Runnable, que la méthode en question utilisé par défaut. Pour le test unitaire, nous avons défini un Runnable simulé différent. Quelque chose comme ça:

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

... et la méthode de test JUnit ...

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

Est-ce ce qu'ils appellent l'injection de dépendance?
Thomas Ahle

2
Cet exemple, tel qu'il est écrit, est une sorte d'injection de dépendance à moitié cuite - la dépendance est transmise à la méthode foo visible par le package (par la méthode foo publique ou le test unitaire), mais la classe principale code toujours en dur l'implémentation Runnable par défaut.
Scott Bale

6

Créez une classe factice qui enveloppe System.exit ()

Je suis d'accord avec EricSchaefer . Mais si vous utilisez un bon cadre moqueur comme Mockito une simple classe concrète suffit, pas besoin d'une interface et de deux implémentations.

Arrêt de l'exécution des tests sur System.exit ()

Problème:

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

Un simulé Sytem.exit()ne mettra pas fin à l'exécution. C'est mauvais si vous voulez tester qui thing2n'est pas exécuté.

Solution:

Vous devriez refactoriser ce code comme suggéré par Martin :

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

Et faites System.exit(status)dans la fonction d'appel. Cela vous oblige à avoir tous vos System.exit()s en un seul endroit dans ou à proximité main(). C'est plus propre que d'appeler System.exit()profondément dans votre logique.

Code

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

Tester:

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

J'aime certaines des réponses déjà données, mais je voulais démontrer une technique différente qui est souvent utile lors de la mise à l'essai du code hérité. Étant donné le code comme:

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

Vous pouvez effectuer une refactorisation sûre pour créer une méthode qui encapsule l'appel System.exit:

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

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

Ensuite, vous pouvez créer un faux pour votre test qui remplace la sortie:

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

Il s'agit d'une technique générique pour substituer le comportement aux cas de test, et je l'utilise tout le temps lors de la refactorisation du code hérité. Ce n'est généralement pas là où je vais laisser quelque chose, mais une étape intermédiaire pour tester le code existant.


2
Cette technique n'arrête pas le flux de contrôle lorsque la sortie () a été appelée. Utilisez une exception à la place.
Andrea Francia

3

Un rapide coup d'œil à l'API, montre que System.exit peut lever une exception esp. si un gestionnaire de sécurité interdit l'arrêt du VM. Une solution serait peut-être d'installer un tel gestionnaire.


3

Vous pouvez utiliser le java SecurityManager pour empêcher le thread actuel d'arrêter la machine virtuelle Java. Le code suivant devrait faire ce que vous voulez:

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. Les documents System.exit indiquent spécifiquement que checkExit (int) sera appelé, pas checkPermission avec name = "exitVM". Je me demande si je devrais remplacer les deux?
Chris Conway

Le nom de l'autorisation semble en fait être exitVM. (Statuscode), c'est-à-dire exitVM.0 - au moins dans mon récent test sur OSX.
Mark Derricutt

3

Pour que la réponse de VonC s'exécute sur JUnit 4, j'ai modifié le code comme suit

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

Vous pouvez tester System.exit (..) en remplaçant l'instance Runtime. Par exemple avec 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

Il existe des environnements où le code de sortie renvoyé est utilisé par le programme appelant (tel que ERRORLEVEL dans MS Batch). Nous avons des tests sur les principales méthodes qui le font dans notre code, et notre approche a été d'utiliser un remplacement SecurityManager similaire à celui utilisé dans d'autres tests ici.

Hier soir, j'ai monté un petit JAR en utilisant les annotations Junit @Rule pour masquer le code du gestionnaire de sécurité, ainsi que pour ajouter des attentes basées sur le code de retour attendu. http://code.google.com/p/junitsystemrules/


2

La plupart des solutions

  • terminer le test (méthode, pas la totalité du cycle) le moment System.exit()est appelé
  • ignorer un déjà installé SecurityManager
  • Parfois, être assez spécifique à un framework de test
  • restreindre pour être utilisé au maximum une fois par scénario de test

Ainsi, la plupart des solutions ne conviennent pas aux situations où:

  • La vérification des effets secondaires doit être effectuée après l'appel à System.exit()
  • Un gestionnaire de sécurité existant fait partie des tests.
  • Un cadre de test différent est utilisé.
  • Vous souhaitez avoir plusieurs vérifications dans un même scénario de test. Cela peut être strictement déconseillé, mais peut parfois être très pratique, en particulier en combinaison avec assertAll(), par exemple.

Je n'étais pas satisfait des restrictions imposées par les solutions existantes présentées dans les autres réponses, et j'ai donc trouvé quelque chose par moi-même.

La classe suivante fournit une méthode assertExits(int expectedStatus, Executable executable)qui affirme qu'elle System.exit()est appelée avec un spécifiéstatus valeur et le test peut continuer après. Cela fonctionne de la même manière que JUnit 5assertThrows . Il respecte également un gestionnaire de sécurité existant.

Il reste un problème: lorsque le code sous test installe un nouveau gestionnaire de sécurité qui remplace complètement le gestionnaire de sécurité défini par le test. Toutes les autres SecurityManagersolutions à ma connaissance connaissent le même problème.

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

Vous pouvez utiliser la classe comme ceci:

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

Le code peut facilement être porté sur JUnit 4, TestNG ou tout autre framework, si nécessaire. Le seul élément spécifique au framework échoue au test. Cela peut facilement être changé en quelque chose d'indépendant du framework (autre qu'un Junit 4 Rule

Il y a place à amélioration, par exemple, la surcharge assertExits()de messages personnalisables.


1

Permet Runtime.exec(String command)de démarrer la JVM dans un processus distinct.


3
Comment allez-vous interagir avec la classe testée, si elle est dans un processus distinct du test unitaire?
DNA

1
Dans 99% des cas, System.exit fait partie d'une méthode principale. Par conséquent, vous interagissez avec eux par des arguments de ligne de commande (c'est-à-dire des arguments).
L. Holanda

1

Il y a un problème mineur avec la SecurityManagersolution. Certaines méthodes, telles que JFrame.exitOnClose, appellent également SecurityManager.checkExit. Dans mon application, je ne voulais pas que cet appel échoue, j'ai donc utilisé

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

0

Appeler System.exit () est une mauvaise pratique, sauf si cela se fait à l'intérieur d'un main (). Ces méthodes devraient lever une exception qui, finalement, est interceptée par votre main (), qui appelle ensuite System.exit avec le code approprié.


8
Mais cela ne répond pas à la question. Et si la fonction testée est finalement la méthode principale? Donc, appeler System.exit () peut être valide et correct du point de vue de la conception. Comment écrivez-vous un cas de test pour cela?
Elie

3
Vous ne devriez pas avoir à tester la méthode principale car la méthode principale doit simplement prendre tous les arguments, les passer à une méthode d'analyse syntaxique, puis démarrer l'application. Il ne devrait pas y avoir de logique dans la méthode principale à tester.
Thomas Owens

5
@Elie: Dans ces types de questions, il y a deux réponses valides. L'un répondant à la question posée et l'autre demandant pourquoi la question était fondée. Les deux types de réponses permettent une meilleure compréhension, et surtout les deux ensemble.
runaros
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.