Java: Comment tester les méthodes qui appellent le système.exit ()?


J'ai quelques méthodes qui devraient appeler System.exit() sur certaines entrées. Malheureusement, tester ces cas entraîne la fin de JUnit! Mettre les appels de méthode dans un nouveau thread ne semble pas aider, car System.exit() termine la JVM, pas seulement le thread actuel. Existe-il des modèles communs pour faire face à cela? Par exemple, puis-je substituer un talon pour System.exit()?

[EDIT] La classe en question est en fait un outil de ligne de commande que j'essaie de tester dans JUnit. Peut-être que JUnit est 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).

Author: EricSchaefer, 2008-11-21

15 answers

En Effet, Derkeiler.com suggère:

  • Pourquoi System.exit() ?

Au lieu de se terminer par le système.exit (whateverValue), pourquoi ne pas lancer une exception non cochée? En utilisation normale, il dérivera jusqu'au dernier receveur 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 pris par le cadre JUnit, qui rapportera que tel-et-tel test a échoué et se déplacer en douceur le long de la prochaine.

  • Empêcher 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 le système.quittez, puis attrapez l'exception 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 décembre 2012:

Will propose dans les commentaires en utilisant Règles du Système, une collection de règles JUnit (4.9+) pour les tests code qui utilise java.lang.System.
Cela a été initialement mentionné par L'Équipe de France dans sa réponse en décembre 2011.

System.exit(…)

Utiliser le ExpectedSystemExit la règle 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);
    }
}
 184
Author: VonC, 2017-05-23 11:55:03

La bibliothèque System Rules a une règle JUnit appelée ExpectedSystemExit. Avec cette règle, vous pouvez tester le code, qui appelle le système.sortie(...):

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 de cette bibliothèque.

 77
Author: Stefan Birkner, 2015-11-11 10:54:08

Vous pouvez réellement simuler ou tronquer la méthode System.exit, dans un test JUnit.

Par exemple, en utilisant JMockit vous pouvez é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 (en 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)");
}
 30
Author: Rogério, 2015-10-21 20:41:23

Que diriez-vous d'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.

 29
Author: EricSchaefer, 2008-11-21 16:59:19

Une astuce que nous avons utilisée dans notre base de code était d'avoir l'appel au système.exit() soit encapsulé dans un impl exécutable, que la méthode en question utilisait par défaut. Pour tester l'unité, nous définissons une simulation différente Runnable. 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()); 
}
 20
Author: Scott Bale, 2008-12-13 20:32:28

Créez une classe simulée qui enveloppe le système.exit ()

Je suis d'accord avec EricSchaefer. Mais si vous utilisez un bon framework 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 du test sur le système.exit ()

Problème:

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

Un Sytem.exit() moqué ne mettra pas fin à l'exécution. C'est mauvais si vous voulez tester que thing2 n'est pas exécuter.

Solution:

, Vous devriez refactoriser ce code comme suggéré par martin:

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

Et faire System.exit(status) dans la fonction appelante. Cela vous oblige à avoir tous vos System.exit()en un seul endroit dans ou près de main(). C'est plus propre que d'appeler System.exit() au plus profond de votre logique.

Code

Wrapper:

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

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
Author: Arend v. Reinersdorff, 2017-05-23 11:55:03

J'aime certaines des réponses déjà données mais je voulais démontrer une technique différente qui est souvent utile lors du test de code hérité. Code donné comme:

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

Vous pouvez effectuer un refactoring sécurisé pour créer une méthode qui enveloppe le système.appel de sortie:

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 exit:

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

C'est 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 la chose, mais une étape intermédiaire pour tester le code existant.

 4
Author: Jeffrey Fredrick, 2008-11-21 18:31:29

Un rapide coup d'oeil à l'api, montre ce système.exit peut lancer une exception esp. si un securitymanager interdit l'arrêt de la machine virtuelle. Peut-être qu'une solution serait d'installer un tel gestionnaire.

 3
Author: flolo, 2008-11-21 16:45:07

Vous pouvez utiliser 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);
 3
Author: Marc Novakowski, 2008-11-21 16:55:13

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
Author: Jeow Li Huan, 2010-01-31 17:45:23

Il existe des environnements où le code de sortie renvoyé est utilisé par le programme appelant (comme ERRORLEVEL dans MS Batch). Nous avons des tests autour des 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 mis en place un petit POT en utilisant les annotations Junit @Rule pour masquer le code du gestionnaire de sécurité, ainsi que pour ajouter des attentes en fonction du code de retour attendu. http://code.google.com/p/junitsystemrules/

 2
Author: Dan Watt, 2010-06-22 20:08:21

Vous pouvez tester le système.sortie(..) avec remplacement de l'instance d'exécution. 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
Author: ursa, 2013-06-11 19:51:55

Système d'appel.exit () est une mauvaise pratique, sauf si cela se fait à l'intérieur d'une main (). Ces méthodes devraient lancer une exception qui, en fin de compte, est interceptée par votre main (), qui appelle ensuite System.quittez avec le code approprié.

 1
Author: , 2008-11-21 16:50:42

Utilisez Runtime.exec(String command) pour démarrer la JVM dans un processus séparé.

 1
Author: Alexei, 2012-02-20 14:19:20

Il y a un problème mineur avec la solution SecurityManager. Certaines méthodes, telles que JFrame.exitOnClose, aussi appel 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);
 1
Author: cayhorstmann, 2012-03-27 23:49:01