237 votes

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

J'ai quelques méthodes qui devraient appeler System.exit() sur certaines entrées. Malheureusement, le test de ces cas provoque l'arrêt de JUnit ! Placer les appels de méthode dans un nouveau Thread ne semble pas aider, puisque System.exit() met fin à la JVM, et pas seulement au thread en cours. Existe-t-il des modèles communs pour traiter ce problème ? Par exemple, puis-je substituer un stub à 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 ce 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 votes

Je suis curieux de savoir pourquoi une fonction appellerait System.exit()...

2 votes

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

5 votes

Je continue à penser que dans ce cas, il devrait y avoir une façon plus agréable de quitter l'application plutôt que System.exit().

239voto

VonC Points 414372

En effet, Derkeiler.com suggère :

  • Pourquoi System.exit() ?

Au lieu de terminer par System.exit(whateverValue), pourquoi ne pas lancer une exception non vérifiée ? Dans une utilisation normale, elle dérivera jusqu'à l'attrapeur de dernière chance de la JVM et arrêtera votre script (à moins que vous ne décidiez de l'attraper quelque part en cours de route, ce qui pourrait s'avérer utile un jour).

Dans le scénario JUnit, il sera détecté par le programme JUnit fr. tel ou tel test a échoué et passera en douceur au suivant.

  • Prévenir System.exit() pour quitter la JVM :

Essayez de modifier le TestCase pour qu'il s'exécute avec un gestionnaire de sécurité qui empêche l'appel à System.exit, puis attrapez l'exception de sécurité.

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 décembre 2012 :

Volonté propose dans les commentaires en utilisant Règles du système un ensemble de règles JUnit (4.9+) pour tester le code qui utilise java.lang.System .
Cela a été mentionné initialement par Stefan Birkner en sa réponse en décembre 2011.

System.exit(…)

Utiliser le ExpectedSystemExit pour vérifier que System.exit(…) est appelé.
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 votes

Je n'ai pas aimé la première réponse, mais la seconde est très intéressante. Je n'avais jamais utilisé de gestionnaires de sécurité et je pensais qu'ils étaient plus compliqués que cela. Cependant, comment tester le gestionnaire de sécurité/mécanisme de test.

8 votes

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

6 votes

Je n'aime pas la solution qui consiste à utiliser le gestionnaire de sécurité. Il me semble que c'est un piratage pour le tester.

136voto

Stefan Birkner Points 3080

La bibliothèque Système Lambda a une méthode catchSystemExit Cette règle permet de tester le code qui appelle System.exit(...) :

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

    @Test
    public void systemExitWithSelectedStatusCode0() {
        int status = SystemLambda.catchSystemExit(() -> {
            //the code under test, which calls System.exit(0);
        });

        assertEquals(0, status);
    }
}

Pour Java 5 à 7, la bibliothèque Règles du système possède une règle JUnit appelée ExpectedSystemExit. Cette règle permet de tester le code qui appelle System.exit(...) :

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

Révélation complète : je suis l'auteur des deux bibliothèques.

5 votes

Parfait. Élégant. Je n'ai pas eu à changer un point de mon code original ni à jouer avec le gestionnaire de sécurité. Cela devrait être la meilleure réponse !

4 votes

Cela devrait être la première réponse. Au lieu d'un débat interminable sur l'utilisation correcte de System.exit, il faut aller droit au but. De plus, une solution flexible en accord avec JUnit lui-même afin que nous n'ayons pas à réinventer la roue ou à nous embrouiller avec le gestionnaire de sécurité.

0 votes

@LeoHolanda Cette solution ne souffre-t-elle pas du même "problème" que celui que vous avez utilisé pour justifier un downvote sur ma réponse ? Par ailleurs, qu'en est-il des utilisateurs de TestNG ?

33voto

EricSchaefer Points 7592

Pourquoi ne pas injecter un "ExitManager" dans cette méthode :

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 votes

+1 Belle solution. Elle est également facile à mettre en œuvre si vous utilisez Spring car l'ExitManager devient un simple Component. Sachez simplement que vous devez vous assurer que votre code ne continue pas à s'exécuter après l'appel de exitManager.exit(). Lorsque votre code est testé avec un ExitManager fictif, le code ne sortira pas réellement après l'appel à exitManager.exit.

32voto

Rogério Points 5460

En fait, vous puede se moquer de la System.exit dans un test JUnit.

Par exemple, en utilisant JMockit vous pourriez écrire (il y a aussi 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 n'autorise aucun code à 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 votes

Raison du vote négatif : 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 de la condition if), le code continuera à s'exécuter.

0 votes

@LeoHolanda Ajout d'une version du test qui empêche le code d'être exécuté après l'entrée de la fonction exit (ce qui n'était pas vraiment un problème, d'après l'IMO).

0 votes

Ne serait-il pas plus approprié de remplacer le dernier System.out.println() par une déclaration assert ?

21voto

Scott Bale Points 4385

Une astuce que nous avons utilisée dans notre base de code consistait à encapsuler l'appel à System.exit() dans un impl Runnable, que la méthode en question utilisait par défaut. Pour les tests unitaires, nous avons mis en place un Runnable fictif différent. Quelque chose comme ceci :

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

0 votes

C'est ce qu'on appelle l'injection de dépendance ?

2 votes

Cet exemple, tel qu'il est écrit, est une sorte d'injection de dépendance en demi-teinte - la dépendance est passée à la méthode foo visible dans le paquet (soit par la méthode foo publique, soit par le test unitaire), mais la classe principale code toujours en dur l'implémentation par défaut de Runnable.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X