Java: Comment tester des 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 entraîne la fin de JUnit! Mettre les appels de méthode dans un nouveau thread ne semble pas aider, puisque System.exit() termine la JVM, pas seulement le thread en cours. Y a-t-il des schémas communs pour gérer cela? Par exemple, puis-je remplacer un stub par System.exit() ?

[EDIT] La classe en question est en fait un outil de ligne de commande que je tente 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).

En effet, Derkeiler.com suggère:

  • Pourquoi System.exit() ?

Au lieu de terminer avec System.exit (QuelleValeur), pourquoi ne pas lancer une exception non contrôlée? En utilisation normale, il va dériver jusqu’au dernier capteur de la JVM et arrêter votre script (à moins que vous ne décidiez de l’attraper quelque part, ce qui pourrait être utile un jour).

Dans le scénario JUnit, il sera pris en charge par le framework JUnit, qui signalera que tel test a échoué et s’est déroulé sans heurts.

  • Empêcher System.exit() de quitter réellement la JVM:

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

 System.exit(…) 

Utilisez la règle ExpectedSystemExit pour vérifier que System.exit(…) est appelé.
Vous pouvez également vérifier le statut 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); } } 

La bibliothèque System Rules a 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 de cette bibliothèque.

Vous pouvez réellement simuler ou extraire 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 (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)"); } 

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.

Une astuce que nous avons utilisée dans notre base de code était que l’appel à System.exit () soit encapsulé dans un implémentation Runnable, que la méthode en question utilisait par défaut. Pour tester l’unité, nous définissons un autre Runnable 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()); } 

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

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

Arrêt de l’exécution du test sur System.exit ()

Problème:

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

Un Sytem.exit() ne mettra pas fin à l’exécution. C’est mauvais si vous voulez tester que thing2 n’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 appelante. Cela vous oblige à avoir tous vos System.exit() s dans un seul endroit dans ou près de main() . C’est plus propre que d’appeler System.exit() profondément dans votre logique.

Code

Emballage:

 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(Ssortingng[] args) { SystemExit aSystemExit = new SystemExit(); Main main = new Main(aSystemExit); main.executeAndExit(args); } void executeAndExit(Ssortingng[] args) { int status = execute(args); systemExit.exit(status); } private int execute(Ssortingng[] 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() { Ssortingng[] emptyArgs = {}; // test main.executeAndExit(emptyArgs); verify(systemExit).exit(1); } } 

J’aime certaines des réponses déjà fournies, mais je voulais démontrer une technique différente qui est souvent utile lors du test du 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 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 permettant de 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à que je vais laisser quelque chose, mais une étape intermédiaire pour faire tester le code existant.

Un rapide coup d’œil à l’API montre que System.exit peut lancer une exception en particulier. si un gestionnaire de sécurité interdit l’arrêt de la machine virtuelle. Peut-être qu’une solution serait d’installer un tel gestionnaire.

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

Pour que VonC réponde à 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); } 

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 autour des principales méthodes qui font cela dans notre code, et notre approche a consisté à utiliser une substitution SecurityManager similaire à celle utilisée dans d’autres tests ici.

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

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

L’appel de System.exit () est une mauvaise pratique, sauf si cela se fait à l’intérieur d’un main (). Ces méthodes doivent générer une exception qui sera finalement interceptée par votre main (), qui appelle alors System.exit avec le code approprié.

Utilisez Runtime.exec(Ssortingng command) pour démarrer JVM dans un processus distinct.

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

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