web-dev-qa-db-fra.com

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, tester ces cas provoque l'arrêt de JUnit! Mettre les appels de méthode dans un nouveau thread ne semble pas aider, puisque System.exit() met fin à la JVM, pas seulement au thread actuel. Existe-t-il des modèles communs pour traiter ce problème? Par exemple, puis-je remplacer un stub pour 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 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).

181
Chris Conway

En effet, Derkeiler.com suggère:

  • Pourquoi System.exit()?

Au lieu de terminer avec System.exit (quelle que soit la valeur), pourquoi ne pas lancer une exception non contrôlée? En utilisation normale, il dérive jusqu'au dernier réceptacle de la JVM et ferme votre script (à moins que vous ne décidiez de le capturer quelque part en cours de route, ce qui pourrait être utile un jour).

Dans le scénario JUnit, il sera capturé par la structure JUnit, qui signalera que tel test a échoué et passe sans problème au suivant.

  • Empêchez System.exit() de quitter réellement la machine virtuelle Java:

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 attrapez 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 en utilisant Règles système , une collection des règles JUnit (4.9+) pour tester le code qui utilise Java.lang.System.
Cela avait été mentionné initialement 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);
    }
}
200
VonC

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

95
Stefan Birkner

En fait pouvez simuler ou remplacer la méthode System.exit Dans un test JUnit.

Par exemple, en utilisant JMockit vous pourriez écrire (il existe 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 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)");
}
31
Rogério

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.

31
EricSchaefer

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 tester l’unité, nous définissons un modèle différent Runnable. 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()); 
}
20
Scott Bale

Créer une classe modulable qui englobe System.exit ()

Je suis d'accord avec EricSchaefer . Mais si vous utilisez un bon framework moqueur tel que Mockito une simple classe concrète suffit, aucun besoin d’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() simulé 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 endroit proche ou dans 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);
    }
}

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

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

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

Vous pouvez effectuer une refactorisation en toute sécurité pour créer une méthode qui englobe 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 annule 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 à des cas de test, et je l'utilise tout le temps lors du refactoring du code hérité. Ce n’est généralement pas là où je vais laisser quelque chose, mais une étape intermédiaire pour obtenir le code existant sous test.

5
Jeffrey Fredrick

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

3
flolo

Vous pouvez utiliser le Java SecurityManager pour empêcher le thread actuel d’arrêter la Java VM. Le code suivant doit 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
Marc Novakowski

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
Jeow Li Huan

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
ursa

Il existe des environnements dans lesquels le code de sortie renvoyé est utilisé par le programme appelant (tel que ERRORLEVEL dans MS Batch). Notre code contient des tests sur les principales méthodes utilisées dans ce but, et notre approche a consisté à utiliser un remplacement de SecurityManager similaire à celui utilisé dans d'autres tests ici.

Hier soir, j'ai mis au point un petit fichier JAR utilisant des 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
Dan Watt

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

1
Alexei

Il y a un problème mineur avec la solution SecurityManager. Certaines méthodes, telles que JFrame.exitOnClose, appelez aussi 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);
1
cayhorstmann

L'appel de System.exit () est une mauvaise pratique, à moins que cela ne soit fait à l'intérieur d'un main (). Ces méthodes devraient générer une exception qui, en fin de compte, sera capturée par votre main (), qui appelle ensuite System.exit avec le code approprié.

1
martin