JUnit-Test für System.out.println ()

370

Ich muss JUnit-Tests für eine alte Anwendung schreiben, die schlecht gestaltet ist und viele Fehlermeldungen in die Standardausgabe schreibt. Wenn sich die getResponse(String request)Methode korrekt verhält, wird eine XML-Antwort zurückgegeben:

@BeforeClass
public static void setUpClass() throws Exception {
    Properties queries = loadPropertiesFile("requests.properties");
    Properties responses = loadPropertiesFile("responses.properties");
    instance = new ResponseGenerator(queries, responses);
}

@Test
public void testGetResponse() {
    String request = "<some>request</some>";
    String expResult = "<some>response</some>";
    String result = instance.getResponse(request);
    assertEquals(expResult, result);
}

Wenn es jedoch fehlerhaftes XML erhält oder die Anforderung nicht versteht, gibt es nulleinige Daten zurück und schreibt sie in die Standardausgabe.

Gibt es eine Möglichkeit, die Konsolenausgabe in JUnit zu aktivieren? Um Fälle zu fangen wie:

System.out.println("match found: " + strExpr);
System.out.println("xml not well formed: " + e.getMessage());
Mike Minicki
quelle
Bezogen auf, aber kein Duplikat von stackoverflow.com/questions/3381801/…
Raedwald

Antworten:

581

Die Verwendung von ByteArrayOutputStream und System.setXXX ist einfach:

private final ByteArrayOutputStream outContent = new ByteArrayOutputStream();
private final ByteArrayOutputStream errContent = new ByteArrayOutputStream();
private final PrintStream originalOut = System.out;
private final PrintStream originalErr = System.err;

@Before
public void setUpStreams() {
    System.setOut(new PrintStream(outContent));
    System.setErr(new PrintStream(errContent));
}

@After
public void restoreStreams() {
    System.setOut(originalOut);
    System.setErr(originalErr);
}

Beispiel Testfälle:

@Test
public void out() {
    System.out.print("hello");
    assertEquals("hello", outContent.toString());
}

@Test
public void err() {
    System.err.print("hello again");
    assertEquals("hello again", errContent.toString());
}

Ich habe diesen Code verwendet, um die Befehlszeilenoption zu testen (wobei behauptet wurde, dass -version die Versionszeichenfolge usw. usw. ausgibt).

Bearbeiten: Frühere Versionen dieser Antwort wurden System.setOut(null)nach den Tests aufgerufen . Dies ist die Ursache für NullPointerExceptions, auf die sich Kommentatoren beziehen.

dfa
quelle
Außerdem habe ich JUnitMatchers verwendet, um auf Antworten zu testen: assertThat (result, enthältString ("<request: GetEmployeeByKeyResponse")); Danke, dfa.
Mike Minicki
3
Ich bevorzuge die Verwendung von System.setOut (null), um den Stream wieder auf den Stand der VM zurückzusetzen
tddmonkey
5
Die Javadocs sagen nichts darüber aus, dass sie Null an System.setOut oder System.setErr übergeben können. Sind Sie sicher, dass dies bei allen JREs funktioniert?
Finnw
55
Ich bin NullPointerExceptionin anderen Tests auf einen gestoßen , nachdem ich einen Nullfehlerstrom wie oben vorgeschlagen festgelegt hatte (in java.io.writer(Object), intern von einem XML-Validator aufgerufen). Ich würde vorschlagen, stattdessen das Original in einem Feld zu speichern oldStdErr = System.errund dies in der @AfterMethode wiederherzustellen .
Luke Usherwood
6
Tolle Lösung. Nur ein Hinweis für jeden, der es verwendet. Möglicherweise müssen Sie Leerzeichen / Zeilenumbrüche von outContent kürzen ().
Allison
102

Ich weiß, dass dies ein alter Thread ist, aber es gibt eine nette Bibliothek, um dies zu tun:

Systemregeln

Beispiel aus den Dokumenten:

public void MyTest {
    @Rule
    public final SystemOutRule systemOutRule = new SystemOutRule().enableLog();

    @Test
    public void overrideProperty() {
        System.out.print("hello world");
        assertEquals("hello world", systemOutRule.getLog());
    }
}

Sie können damit auch Trap System.exit(-1)und andere Dinge abfangen, auf die ein Befehlszeilentool getestet werden müsste.

Wille
quelle
1
Dieser Ansatz ist mit Problemen behaftet, da der Standardausgabestream eine gemeinsam genutzte Ressource ist, die von allen Teilen Ihres Programms verwendet wird. Es ist besser, Dependency Injection zu verwenden, um die direkte Verwendung des Standardausgabestreams zu eliminieren: stackoverflow.com/a/21216342/545127
Raedwald
30

Anstatt umzuleiten System.out, würde ich die verwendete Klasse umgestalten, System.out.println()indem ich a PrintStreamals Kollaborateur übergebe und dann System.outin der Produktion und einen Testspion im Test verwende. Verwenden Sie also Dependency Injection, um die direkte Verwendung des Standardausgabestreams zu vermeiden.

In Produktion

ConsoleWriter writer = new ConsoleWriter(System.out));

Im Test

ByteArrayOutputStream outSpy = new ByteArrayOutputStream();
ConsoleWriter writer = new ConsoleWriter(new PrintStream(outSpy));
writer.printSomething();
assertThat(outSpy.toString(), is("expected output"));

Diskussion

Auf diese Weise kann die zu testende Klasse durch einfaches Refactoring getestet werden, ohne dass eine indirekte Umleitung der Standardausgabe oder ein unklares Abfangen mit einer Systemregel erforderlich ist.

user1909402
quelle
1
Ich konnte diesen ConsoleWriter nirgendwo im JDK finden: Wo ist er?
Jean-Philippe Caruana
3
Es sollte wahrscheinlich in der Antwort erwähnt werden, aber ich glaube, dass die Klasse von user1909402 erstellt wurde.
Sebastian
6
Ich denke, ConsoleWriterist das Testobjekt,
Niel de Wet
22

Sie können den System.out- Druckstrom über setOut () (und für inund err) festlegen . Können Sie dies in einen Druckdatenstrom umleiten, der in eine Zeichenfolge aufzeichnet, und diesen dann überprüfen? Das scheint der einfachste Mechanismus zu sein.

(Ich würde empfehlen, die App irgendwann in ein Protokollierungsframework zu konvertieren - aber ich vermute, dass Sie sich dessen bereits bewusst sind!)

Brian Agnew
quelle
1
Das kam mir in den Sinn, aber ich konnte nicht glauben, dass es keinen Standard-JUnit-Weg gibt, dies zu tun. Danke, Brain. Aber die Credits gingen an dfa für den tatsächlichen Aufwand.
Mike Minicki
Dieser Ansatz ist mit Problemen behaftet, da der Standardausgabestream eine gemeinsam genutzte Ressource ist, die von allen Teilen Ihres Programms verwendet wird. Es ist besser, Dependency Injection zu verwenden, um die direkte Verwendung des Standardausgabestreams zu eliminieren: stackoverflow.com/a/21216342/545127
Raedwald
Ja. Ich würde das unterstützen und vielleicht sogar eine Protokollierungsbestätigung in Frage stellen (besser, um einen Anruf auf eine Protokollierungskomponente oder ähnliches zu bestätigen)
Brian Agnew
13

Etwas abseits des Themas, aber falls einige Leute (wie ich, als ich diesen Thread zum ersten Mal fand) daran interessiert sein könnten, die Protokollausgabe über SLF4J zu erfassen, könnte JUnit von Commons-Testing@Rule helfen:

public class FooTest {
    @Rule
    public final ExpectedLogs logs = new ExpectedLogs() {{
        captureFor(Foo.class, LogLevel.WARN);
    }};

    @Test
    public void barShouldLogWarning() {
        assertThat(logs.isEmpty(), is(true)); // Nothing captured yet.

        // Logic using the class you are capturing logs for:
        Foo foo = new Foo();
        assertThat(foo.bar(), is(not(nullValue())));

        // Assert content of the captured logs:
        assertThat(logs.isEmpty(), is(false));
        assertThat(logs.contains("Your warning message here"), is(true));
    }
}

Haftungsausschluss :

  • Ich habe diese Bibliothek entwickelt, da ich keine geeignete Lösung für meine eigenen Bedürfnisse finden konnte.
  • Nur Bindungen für log4j, log4j2und logbacksind zur Zeit verfügbar, aber ich bin glücklich , mehr hinzuzufügen.
Marc Carré
quelle
Vielen Dank für die Erstellung dieser Bibliothek! Ich habe so lange nach so etwas gesucht! Es ist sehr, sehr nützlich, da Sie Ihren Code manchmal einfach nicht genug vereinfachen können, um leicht getestet werden zu können, aber mit einer Protokollnachricht können Sie Wunder vollbringen!
Carlspring
Das sieht wirklich vielversprechend aus ... aber selbst wenn ich Ihr ATMTest-Programm kopiere und es als Test unter Gradle ausführe, bekomme ich eine Ausnahme ... Ich habe ein Problem auf Ihrer Github-Seite angesprochen ...
Mike Nagetier
9

Die Antwort von @dfa ist großartig, also bin ich noch einen Schritt weiter gegangen, um das Testen von Ausgabeblöcken zu ermöglichen.

Zuerst habe ich TestHelpermit einer Methode erstellt captureOutput, die die nervige Klasse akzeptiert CaptureTest. Die CaptureOutput-Methode übernimmt das Festlegen und Herunterfahren der Ausgabestreams. Wenn die Umsetzung von CaptureOutput‚s - testMethode aufgerufen wird, hat sie Zugriff auf den Ausgang für den Testblock erzeugen.

Quelle für TestHelper:

public class TestHelper {

    public static void captureOutput( CaptureTest test ) throws Exception {
        ByteArrayOutputStream outContent = new ByteArrayOutputStream();
        ByteArrayOutputStream errContent = new ByteArrayOutputStream();

        System.setOut(new PrintStream(outContent));
        System.setErr(new PrintStream(errContent));

        test.test( outContent, errContent );

        System.setOut(new PrintStream(new FileOutputStream(FileDescriptor.out)));
        System.setErr(new PrintStream(new FileOutputStream(FileDescriptor.out)));

    }
}

abstract class CaptureTest {
    public abstract void test( ByteArrayOutputStream outContent, ByteArrayOutputStream errContent ) throws Exception;
}

Beachten Sie, dass TestHelper und CaptureTest in derselben Datei definiert sind.

Anschließend können Sie in Ihrem Test den statischen CaptureOutput importieren. Hier ist ein Beispiel mit JUnit:

// imports for junit
import static package.to.TestHelper.*;

public class SimpleTest {

    @Test
    public void testOutput() throws Exception {

        captureOutput( new CaptureTest() {
            @Override
            public void test(ByteArrayOutputStream outContent, ByteArrayOutputStream errContent) throws Exception {

                // code that writes to System.out

                assertEquals( "the expected output\n", outContent.toString() );
            }
        });
}
mguymon
quelle
7

Wenn Sie Spring Boot verwenden (Sie haben erwähnt, dass Sie mit einer alten Anwendung arbeiten, also wahrscheinlich nicht, aber für andere von Nutzen sein könnten), können Sie org.springframework.boot.test.rule.OutputCapture verwenden auf folgende Art:

@Rule
public OutputCapture outputCapture = new OutputCapture();

@Test
public void out() {
    System.out.print("hello");
    assertEquals(outputCapture.toString(), "hello");
}
Disper
quelle
1
Ich habe Ihre Antwort positiv bewertet, weil ich Spring Boot verwende und damit auf dem richtigen Weg bin. Vielen Dank! OutputCapture muss jedoch initialisiert werden. (public OutputCapture outputCapture = new OutputCapture ();) Siehe docs.spring.io/spring-boot/docs/current/reference/html/…
EricGreg
Du bist absolut richtig. Danke für den Kommentar! Ich habe meine Antwort aktualisiert.
Disper
4

Basierend auf der Antwort von @ dfa und einer weiteren Antwort, die zeigt, wie System.in getestet wird , möchte ich meine Lösung freigeben , um einem Programm eine Eingabe zu geben und seine Ausgabe zu testen.

Als Referenz verwende ich JUnit 4.12.

Angenommen, wir haben dieses Programm, das einfach Eingabe zu Ausgabe repliziert:

import java.util.Scanner;

public class SimpleProgram {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.print(scanner.next());
        scanner.close();
    }
}

Um es zu testen, können wir die folgende Klasse verwenden:

import static org.junit.Assert.*;

import java.io.*;

import org.junit.*;

public class SimpleProgramTest {
    private final InputStream systemIn = System.in;
    private final PrintStream systemOut = System.out;

    private ByteArrayInputStream testIn;
    private ByteArrayOutputStream testOut;

    @Before
    public void setUpOutput() {
        testOut = new ByteArrayOutputStream();
        System.setOut(new PrintStream(testOut));
    }

    private void provideInput(String data) {
        testIn = new ByteArrayInputStream(data.getBytes());
        System.setIn(testIn);
    }

    private String getOutput() {
        return testOut.toString();
    }

    @After
    public void restoreSystemInputOutput() {
        System.setIn(systemIn);
        System.setOut(systemOut);
    }

    @Test
    public void testCase1() {
        final String testString = "Hello!";
        provideInput(testString);

        SimpleProgram.main(new String[0]);

        assertEquals(testString, getOutput());
    }
}

Ich werde nicht viel erklären, weil ich glaube, dass der Code lesbar ist und ich meine Quellen zitiert habe.

Wenn JUnit ausgeführt testCase1()wird, werden die Hilfsmethoden in der Reihenfolge aufgerufen, in der sie angezeigt werden:

  1. setUpOutput()wegen der @BeforeAnmerkung
  2. provideInput(String data), angerufen von testCase1()
  3. getOutput(), angerufen von testCase1()
  4. restoreSystemInputOutput()wegen der @AfterAnmerkung

Ich habe nicht getestet, System.errweil ich es nicht brauchte, aber es sollte einfach zu implementieren sein, ähnlich wie beim Testen System.out.

Antonio Vinicius Menezes Medei
quelle
1

Sie möchten den Stream system.out nicht umleiten, da dieser für die GESAMTE JVM umleitet. Alles andere, was auf der JVM läuft, kann durcheinander geraten. Es gibt bessere Möglichkeiten, die Eingabe / Ausgabe zu testen. Schauen Sie in Stubs / Mocks.

Sam Jacobs
quelle
1

Vollständiges JUnit 5-Beispiel zum Testen System.out(ersetzen Sie das when-Teil):

package learning;

import static org.assertj.core.api.BDDAssertions.then;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class SystemOutLT {

    private PrintStream originalSystemOut;
    private ByteArrayOutputStream systemOutContent;

    @BeforeEach
    void redirectSystemOutStream() {

        originalSystemOut = System.out;

        // given
        systemOutContent = new ByteArrayOutputStream();
        System.setOut(new PrintStream(systemOutContent));
    }

    @AfterEach
    void restoreSystemOutStream() {
        System.setOut(originalSystemOut);
    }

    @Test
    void shouldPrintToSystemOut() {

        // when
        System.out.println("example");

        then(systemOutContent.toString()).containsIgnoringCase("example");
    }
}
Jens Piegsa
quelle
0

Sie können nicht direkt mit system.out.println oder mit logger api drucken, während Sie JUnit verwenden . Wenn Sie jedoch Werte überprüfen möchten, können Sie diese einfach verwenden

Assert.assertEquals("value", str);

Es wird unter Assertionsfehler geworfen:

java.lang.AssertionError: expected [21.92] but found [value]

Ihr Wert sollte 21,92 sein. Wenn Sie nun mit diesem Wert wie unten testen, besteht Ihr Testfall.

 Assert.assertEquals(21.92, str);
Affy
quelle
0

für raus

@Test
void it_prints_out() {

    PrintStream save_out=System.out;final ByteArrayOutputStream out = new ByteArrayOutputStream();System.setOut(new PrintStream(out));

    System.out.println("Hello World!");
    assertEquals("Hello World!\r\n", out.toString());

    System.setOut(save_out);
}

für err

@Test
void it_prints_err() {

    PrintStream save_err=System.err;final ByteArrayOutputStream err= new ByteArrayOutputStream();System.setErr(new PrintStream(err));

    System.err.println("Hello World!");
    assertEquals("Hello World!\r\n", err.toString());

    System.setErr(save_err);
}
Shimon Doodkin
quelle
Für diese Art von Setup- und Teardown-Logik würde ich eine verwenden @Rule, anstatt sie in Ihrem Test inline zu machen. Insbesondere wenn Ihre Behauptung fehlschlägt, wird der zweite System.setOut/ErrAnruf nicht erreicht.
dimo414