it-swarm.com.de

Java: Wie teste ich Methoden, die System.exit () aufrufen?

Ich habe ein paar Methoden, die System.exit() für bestimmte Eingaben aufrufen sollten. Leider führt das Testen dieser Fälle dazu, dass JUnit beendet wird! Das Einfügen der Methodenaufrufe in einen neuen Thread scheint nicht zu helfen, da System.exit() die JVM beendet, nicht nur den aktuellen Thread. Gibt es gemeinsame Muster, um damit umzugehen? Kann ich beispielsweise einen Stub für System.exit() ersetzen?

[EDIT] Die fragliche Klasse ist eigentlich ein Befehlszeilen-Tool, das ich in JUnit testen möchte. Vielleicht ist JUnit einfach nicht das richtige Werkzeug für den Job? Vorschläge für ergänzende Regressionstest-Tools sind willkommen (vorzugsweise etwas, das sich gut in JUnit und EclEmma integrieren lässt).

181
Chris Conway

In der Tat, Derkeiler.com schlägt vor:

  • Warum System.exit()?

Warum nicht eine ungeprüfte Ausnahme auslösen, anstatt mit System.exit (whateverValue) zu beenden? Bei normaler Verwendung driftet es bis zum letzten Grabenfänger der JVM und beendet Ihr Skript (es sei denn, Sie beschließen, es irgendwo auf dem Weg zu fangen, was eines Tages nützlich sein könnte).

Im JUnit-Szenario wird es vom JUnit-Framework abgefangen, das meldet, dass der eine oder andere Test fehlgeschlagen ist, und nahtlos zum nächsten übergeht.

  • Verhindern Sie, dass System.exit() die JVM tatsächlich beendet:

Versuchen Sie, das TestCase so zu ändern, dass es mit einem Sicherheitsmanager ausgeführt wird, der den Aufruf von System.exit verhindert, und fangen Sie dann die SecurityException ab.

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

Update Dezember 2012:

Will schlägt in den Kommentaren unter Verwendung von Systemregeln eine Sammlung vor of JUnit (4.9+) Regeln zum Testen von Code, der Java.lang.System verwendet.
Dies wurde ursprünglich von Stefan Birkner in seine Antwort im Dezember 2011 erwähnt .

System.exit(…)

Verwenden Sie die Regel ExpectedSystemExit , um zu überprüfen, ob System.exit(…) aufgerufen wird.
Sie können auch den Beendigungsstatus überprüfen.

Zum Beispiel:

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

Die Bibliothek System Rules hat eine JUnit-Regel namens ExpectedSystemExit. Mit dieser Regel können Sie Code testen, der System.exit aufruft (...):

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

Vollständige Offenlegung: Ich bin der Autor dieser Bibliothek.

95
Stefan Birkner

Sie können die System.exit - Methode in einem JUnit-Test verspotten oder herausfiltern.

Zum Beispiel könnten Sie mit JMockit schreiben (es gibt auch andere Möglichkeiten):

@Test
public void mockSystemExit(@Mocked("exit") System mockSystem)
{
    // Called by code under test:
    System.exit(); // will not exit the program
}


EDIT: Alternativer Test (unter Verwendung der neuesten JMockit-API), bei dem nach einem Aufruf von System.exit(n) kein Code ausgeführt werden kann:

@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

Wie wäre es, einen "ExitManager" in diese Methoden einzufügen:

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

Der Produktionscode verwendet den ExitManagerImpl und der Testcode verwendet ExitManagerMock und kann prüfen, ob exit () aufgerufen wurde und mit welchem ​​Exit-Code.

31
EricSchaefer

Ein Trick, den wir in unserer Codebasis verwendeten, bestand darin, den Aufruf von System.exit () in einem Runnable-Impl zu kapseln, das die fragliche Methode standardmäßig verwendet. Zum Unit-Test setzen wir ein anderes Mock-Runnable. Etwas wie das:

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

... und die JUnit-Testmethode ...

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

Erstellen Sie eine verspottbare Klasse, die System.exit () umschließt.

Ich stimme zu EricSchaefer . Wenn Sie jedoch ein gutes Mocking-Framework wie Mockito verwenden, ist eine einfache konkrete Klasse ausreichend, ohne dass eine Schnittstelle und zwei Implementierungen erforderlich sind.

Beenden der Testausführung auf System.exit ()

Problem:

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

Ein gespieltes Sytem.exit() beendet die Ausführung nicht. Dies ist schlecht, wenn Sie testen möchten, dass thing2 Nicht ausgeführt wird.

Lösung:

Sie sollten diesen Code wie von martin vorgeschlagen überarbeiten:

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

Und mache System.exit(status) in der aufrufenden Funktion. Dies zwingt Sie dazu, alle Ihre System.exit() an einem Ort in oder in der Nähe von main() zu haben. Dies ist sauberer, als System.exit() tief in Ihrer Logik aufzurufen.

Code

Verpackung:

public class SystemExit {

    public void exit(int status) {
        System.exit(status);
    }
}

Main:

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

Prüfung:

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

Ich mag einige der bereits gegebenen Antworten, aber ich wollte eine andere Technik demonstrieren, die oft nützlich ist, wenn älterer Code getestet wird. Gegebener Code wie:

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

Sie können ein sicheres Refactoring durchführen, um eine Methode zu erstellen, die den System.exit-Aufruf umschließt:

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

  void exit(int i) {
    System.exit(i);
  }
}

Dann können Sie eine Fälschung für Ihren Test erstellen, die das Beenden überschreibt:

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

Dies ist eine generische Technik, um Testfälle durch Verhalten zu ersetzen, und ich verwende sie die ganze Zeit, wenn ich älteren Code umgestalte. Es ist normalerweise nicht der Ort, an dem ich das Ding verlassen werde, sondern ein Zwischenschritt, um den vorhandenen Code zu testen.

5

Ein kurzer Blick auf die API zeigt, dass System.exit eine Ausnahme auslösen kann. wenn ein securitymanager das herunterfahren des vm verbietet. Vielleicht wäre eine Lösung, einen solchen Manager zu installieren.

3
flolo

Sie können den Java SecurityManager verwenden, um zu verhindern, dass der aktuelle Thread die Java VM herunterfährt. Der folgende Code sollte tun, was Sie wollen:

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

Damit VonCs Antwort unter JUnit 4 ausgeführt werden kann, habe ich den Code wie folgt geändert

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

Sie können System.exit (..) testen, indem Sie die Runtime-Instanz ersetzen. Z.B. mit 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

Es gibt Umgebungen, in denen der zurückgegebene Exit-Code vom aufrufenden Programm verwendet wird (z. B. ERRORLEVEL in MS Batch). Wir haben Tests zu den wichtigsten Methoden durchgeführt, die dies in unserem Code durchführen. Unser Ansatz bestand darin, eine ähnliche SecurityManager-Überschreibung zu verwenden, wie sie in anderen Tests hier verwendet wurde.

Letzte Nacht habe ich eine kleine JAR zusammengestellt, die Junit @ Rule-Annotationen verwendet, um den Sicherheitsmanager-Code auszublenden und Erwartungen basierend auf dem erwarteten Rückkehrcode hinzuzufügen. http://code.google.com/p/junitsystemrules/

2
Dan Watt

Verwenden Sie Runtime.exec(String command), um JVM in einem separaten Prozess zu starten.

1
Alexei

Es gibt ein kleines Problem mit der SecurityManager Lösung. Einige Methoden wie JFrame.exitOnClose, rufen Sie auch SecurityManager.checkExit. In meiner Anwendung wollte ich nicht, dass dieser Aufruf fehlschlägt, also habe ich verwendet

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

Das Aufrufen von System.exit () ist eine schlechte Vorgehensweise, es sei denn, dies erfolgt innerhalb von main (). Diese Methoden sollten eine Ausnahme auslösen, die letztendlich von Ihrem main () abgefangen wird, der dann System.exit mit dem entsprechenden Code aufruft.

1
martin