Java:comment tester des méthodes qui appellent le système.quitter()?

j'ai quelques méthodes qui devraient appeler System.exit() sur certaines entrées. Malheureusement, la mise à l'essai de ces cas amène JUnit à terminer! Mettre les appels de méthode dans un nouveau Thread ne semble pas aider, puisque System.exit() termine le JVM, pas seulement le thread courant. Y a-t-il des tendances communes à ce sujet? Par exemple, puis-je substituer un talon à System.exit() ?

[EDIT] la classe en question Est en fait un outil en ligne de commande que je suis tentative de test à L'intérieur de JUnit. Peut-être que JUnit n'est tout simplement pas le bon outil pour le travail? Les Suggestions pour des outils complémentaires de test de régression sont les bienvenues (de préférence quelque chose qui s'intègre bien avec JUnit et EclEmma).

150
demandé sur EricSchaefer 2008-11-21 19:38:58

15 réponses

en effet, Derkeiler.com suggère:

  • pourquoi System.exit() ?

au lieu de se terminer par le système.sortie(whateverValue), pourquoi ne pas jeter un décoché exception? Dans un usage normal, il dérivera jusqu'au dernier receveur de la JVM et éteindra votre script (à moins que vous ne décidiez de l'attraper quelque part en cours de route, ce qui pourrait être utile un jour).

dans le scénario JUnit, il sera pris au piège par le cadre JUnit, qui signalera que tel-et-tel test échoué et se déplacer en douceur à la prochaine.

  • empêcher System.exit() de sortir effectivement de la JVM:

essayez de modifier le TestCase pour l'exécuter avec un gestionnaire de sécurité qui empêche le système d'appel.sortir, puis attraper la 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:

Sera propose dans les commentaires à l'aide Système de Règles , une collection de JUnit(4.9+) règles pour la vérification du 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 vérifier le statut de sortie, trop.

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
répondu VonC 2017-05-23 11:55:03

La bibliothèque Système de Règles a une JUnit règle appelée ExpectedSystemExit. Avec cette règle vous êtes en mesure de 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 la bibliothèque.

77
répondu Stefan Birkner 2015-11-11 10:54:08

Vous fait peut se moquer ou stub les System.exit la méthode, dans un test JUnit.

par exemple, en utilisant JMockit vous pourriez écrire (il y a d'autres façons aussi):

@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)");
}
30
répondu Rogério 2015-10-21 20:41:23

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 L'ExitManagerImpl et le code de test utilise L'ExitManagerMock et peut vérifier si exit() a été appelé et avec quel code de sortie.

29
répondu EricSchaefer 2008-11-21 16:59:19

Un truc que nous avons utilisé dans notre base de code était d'avoir l'appel Système.exit() être encapsulé dans un Exécutable impl, que la méthode en question est utilisé par défaut. Pour tester l'unité, nous avons paramétré une simulation différente. 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 D'essai de JUnit...

public void testFoo(){   
  final AtomicBoolean actionWasCalled = new AtomicBoolean(false);   
  fooObject.foo(new Runnable(){
    public void run(){
      actionWasCalled.set(true);
    }   
  });   
  assertTrue(actionWasCalled.get()); 
}
20
répondu Scott Bale 2008-12-13 20:32:28

créer une classe de simulation qui enveloppe le système.exit ()

je suis d'accord avec EricSchaefer . Mais si vous utilisez un bon cadre de moquerie comme Mockito une classe de béton simple est suffisant, pas besoin d'une interface et deux implémentations.

arrêt de l'exécution de l'essai sur le système.exit ()

problème:

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

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

Solution:

vous devriez reformuler 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 d'appel. Cela vous oblige à avoir tous vos System.exit() s en un seul endroit dans ou près de main() . C'est plus propre que j'appelle System.exit() au fond de votre logique.

Code

Wrapper:

public class SystemExit {

    public void exit(int status) {
        System.exit(status);
    }
}
"1519130920 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
répondu Arend v. Reinersdorff 2017-05-23 11:55:03

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

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

vous pouvez faire un remaniement sûr 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);
  }
}

alors vous pouvez créer un faux pour votre test qui l'emporte sur 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;
    }
}

il s'agit d'une technique générique de substitution comportement pour les cas de test, et je l'utilise tout le temps lors de la refonte du code d'héritage. Ce n'est généralement pas là où je vais laisser la chose, mais une étape intermédiaire pour mettre le code existant à l'essai.

4
répondu Jeffrey Fredrick 2008-11-21 18:31:29

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

3
répondu flolo 2008-11-21 16:45:07

vous pouvez utiliser le Java SecurityManager pour empêcher le thread courant de désactiver la VM 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
répondu Marc Novakowski 2008-11-21 16:55:13

pour que la réponse de VonC s'exécute le 4 juin, 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
répondu Jeow Li Huan 2010-01-31 17:45:23

il y a des environnements où le code de sortie retourné est utilisé par le programme appelant (comme ERRORLEVEL dans MS Batch). Nous avons des tests sur les principales méthodes de le faire dans notre code, et notre approche a été d'utiliser un SecurityManager remplacer celle utilisée dans d'autres tests ici.

hier soir, j'ai préparé un petit bocal en utilisant les annotations de la règle Junit @pour cacher 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
répondu Dan Watt 2010-06-22 20:08:21

vous pouvez tester le système.sortie.(.) avec remplacer 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
répondu ursa 2013-06-11 19:51:55
Système D'Appel

.exit () est une mauvaise pratique, sauf si elle est faite à l'intérieur d'un main (). Ces méthodes devraient lancer une exception qui, en fin de compte, est attrapée par votre main (), qui appelle alors système.sortie avec le code approprié.

1
répondu 2008-11-21 16:50:42

utiliser Runtime.exec(String command) pour lancer JVM dans un processus distinct.

1
répondu Alexei 2012-02-20 14:19:20

il y a un petit problème avec la solution SecurityManager . Certaines méthodes , comme JFrame.exitOnClose , appellent aussi SecurityManager.checkExit . Dans mon application, Je ne voulais pas que cet appel échoue, donc j'ai utilisé

Class[] stack = getClassContext();
if (stack[1] != JFrame.class && !okToExit) throw new ExitException();
super.checkExit(status);
1
répondu cayhorstmann 2012-03-27 23:49:01