JUnit: Wie simuliert man System.in-Tests?

Lesezeit: 8 Minuten

JUnit Wie simuliert man Systemin Tests
Noppanit

Ich habe ein Java-Befehlszeilenprogramm. Ich möchte einen JUnit-Testfall erstellen, um ihn simulieren zu können System.in. Denn wenn mein Programm läuft, kommt es in die While-Schleife und wartet auf Eingaben von Benutzern. Wie simuliere ich das in JUnit?

Danke

Ein Wechsel ist technisch möglich System.in, aber im Allgemeinen wäre es robuster, es nicht direkt in Ihrem Code aufzurufen, sondern eine indirekte Ebene hinzuzufügen, sodass die Eingabequelle von einem Punkt in Ihrer Anwendung aus gesteuert wird. Wie Sie das genau tun, ist ein Implementierungsdetail – die Vorschläge zur Abhängigkeitsinjektion sind in Ordnung, aber Sie müssen nicht unbedingt Frameworks von Drittanbietern einführen. Sie könnten beispielsweise einen I/O-Kontext aus dem aufrufenden Code herumreichen.

Wie wechseln System.in:

String data = "Hello, World!rn";
InputStream stdin = System.in;
try {
  System.setIn(new ByteArrayInputStream(data.getBytes()));
  Scanner scanner = new Scanner(System.in);
  System.out.println(scanner.nextLine());
} finally {
  System.setIn(stdin);
}

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

Als Referenz verwende ich JUnit 4.12.

Nehmen wir an, wir haben dieses Programm, das einfach die Eingabe in die 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 läuft testCase1()werden die Hilfsmethoden in der Reihenfolge aufgerufen, in der sie erscheinen:

  1. setUpOutput()wegen der @Before Anmerkung
  2. provideInput(String data)angerufen von testCase1()
  3. getOutput()angerufen von testCase1()
  4. restoreSystemInputOutput()wegen der @After Anmerkung

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

  • Wie würden Sie mit mehreren Eingaben fortfahren?

    – Schnurrbart

    4. Juli 18 um 22:28 Uhr

  • Für das, was ich brauchte, war es genug, um eine zu bestehen String mit Newline-Zeichen zu provideInput(String data). Für mein Programm, das auf einer Befehlszeilenschnittstelle ausgeführt wird, gibt der Benutzer jedes Mal Text ein und klickt Enter, wird dieser Text als neue Eingabe behandelt. bitte beachten Sie SimpleProgram verwendet die Java SE Scanner Klasse, um die Eingaben des Benutzers zu lesen.

    – António Medeiros

    5. Juli 18 um 17:27 Uhr


  • Ich hätte also anrufen können provideInput("Input 1nInput 2nInput 3"); stattdessen.

    – António Medeiros

    5. Juli 18 um 17:30 Uhr


JUnit Wie simuliert man Systemin Tests
Yishai

Es gibt einige Möglichkeiten, dies anzugehen. Der vollständigste Weg besteht darin, einen InputStream zu übergeben, während die zu testende Klasse ausgeführt wird, bei dem es sich um einen gefälschten InputStream handelt, der simulierte Daten an Ihre Klasse übergibt. Sie können sich ein Abhängigkeitsinjektions-Framework (wie Google Guice) ansehen, wenn Sie dies häufig in Ihrem Code tun müssen, aber der einfache Weg ist:

 public class MyClass {
     private InputStream systemIn;

     public MyClass() {
         this(System.in);
     }

     public MyClass(InputStream in) {
         systemIn = in;
     }
 }

Im Test würden Sie den Konstruktor aufrufen, der den Eingabestrom entgegennimmt. Sie machen dieses Konstruktorpaket sogar privat und legen den Test in dasselbe Paket, sodass anderer Code im Allgemeinen nicht in Betracht zieht, es zu verwenden.

  • +1. Ich bin da bei dir. Ich würde noch einen Schritt weiter gehen: InputData als High-Level-Wrapper herum InputStream Beim Komponententest sollten Sie sich mehr darum kümmern, was Ihre Klasse tut, und nicht wirklich um die Integration.

    – Oscar Ryz

    30. Oktober 09 um 5:33 Uhr

  • Anstatt die Variable zu erstellen systemIn Sie könnten die Methode verwenden System.setIn(in). Sie können also anrufen System.in normal, aber mit der aktualisierten Version.

    – Larakonda

    22. März 17 um 3:37 Uhr


Versuchen Sie, Ihren Code für die Verwendung umzugestalten Abhängigkeitsspritze. Anstatt eine Methode zu haben, die verwendet System.in direkt, lassen Sie die Methode an akzeptieren InputStream als Argument. Dann kannst du in deinem junit-Test einen Test bestehen InputStream Umsetzung statt System.in.

Sie können einen klaren Test für die Befehlszeilenschnittstelle schreiben, indem Sie die verwenden TextFromStandardInputStream Regel der Systemregeln Bücherei.

public void MyTest {
  @Rule
  public final TextFromStandardInputStream systemInMock
    = emptyStandardInputStream();

  @Test
  public void readTextFromStandardInputStream() {
    systemInMock.provideLines("foo");
    Scanner scanner = new Scanner(System.in);
    assertEquals("foo", scanner.nextLine());
  }
}

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

  • Ich habe gerade versucht, Ihre Bibliothek zu verwenden, was sehr gut schien … aber dann habe ich versucht, sie mit Rule public MockitoRule rule = MockitoJUnit.rule(); zu verwenden, und es schien, soweit ich es versuchte, dass die beiden nicht sein konnten kombiniert … Also konnte ich es nicht mit Injektionen kombinieren …

    – Mike Nagetier

    22. Januar 17 um 18:59 Uhr

  • Das sollte nicht der Fall sein. Könnten Sie bitte ein Problem für die Systemregelbibliothek erstellen und Ihr fehlerhaftes Beispiel angeben.

    – Stefan Birkner

    23. Januar 17 um 22:10 Uhr

  • Danke … jetzt weiß ich, dass es funktionieren soll. Ich werde es untersuchen und gegebenenfalls ein Problem erstellen. Es schien das zu sein TextFromStandardInputStream womit ich ein Problem hatte, aber die SystemOutRule funktionierte OK.

    – Mike Nagetier

    24. Januar 17 um 7:59 Uhr

  • Dies ist eine großartige Bibliothek, aber beachten Sie, dass sie nicht mit junit5 kompatibel ist. Ich musste zu junit4 zurückkehren, um es zum Laufen zu bringen. Es ist ein bekannter Fehler, siehe das Git-Repo, das in Stefans Kommentar verlinkt ist

    – Daniel

    19. Januar 18 um 15:23 Uhr

  • @Daniel, es gibt jetzt System-Lambda, das mit junit5 und Testing kompatibel ist.

    – MiB

    13. September 21 um 3:51 Uhr


Sie könnten eine benutzerdefinierte erstellen InputStream und befestigen Sie es an der System Klasse

class FakeInputStream extends InputStream {

    public int read() {
         return -1;
    }
}

Und dann verwenden Sie es mit Ihrem Scanner

System.in = new FakeInputStream();

Vor:

InputStream in = System.in;
...
Scanner scanner = new Scanner( in );

Nach:

InputStream in = new FakeInputStream();
...
Scanner scanner = new Scanner( in );

Obwohl ich denke, Sie sollten besser testen, wie Ihre Klasse mit den aus dem Eingabestrom gelesenen Daten arbeiten soll und nicht wirklich, wie sie von dort gelesen werden.

  • Ich habe gerade versucht, Ihre Bibliothek zu verwenden, was sehr gut schien … aber dann habe ich versucht, sie mit Rule public MockitoRule rule = MockitoJUnit.rule(); zu verwenden, und es schien, soweit ich es versuchte, dass die beiden nicht sein konnten kombiniert … Also konnte ich es nicht mit Injektionen kombinieren …

    – Mike Nagetier

    22. Januar 17 um 18:59 Uhr

  • Das sollte nicht der Fall sein. Könnten Sie bitte ein Problem für die Systemregelbibliothek erstellen und Ihr fehlerhaftes Beispiel angeben.

    – Stefan Birkner

    23. Januar 17 um 22:10 Uhr

  • Danke … jetzt weiß ich, dass es funktionieren soll. Ich werde es untersuchen und gegebenenfalls ein Problem erstellen. Es schien das zu sein TextFromStandardInputStream womit ich ein Problem hatte, aber die SystemOutRule funktionierte OK.

    – Mike Nagetier

    24. Januar 17 um 7:59 Uhr

  • Dies ist eine großartige Bibliothek, aber beachten Sie, dass sie nicht mit junit5 kompatibel ist. Ich musste zu junit4 zurückkehren, um es zum Laufen zu bringen. Es ist ein bekannter Fehler, siehe das Git-Repo, das in Stefans Kommentar verlinkt ist

    – Daniel

    19. Januar 18 um 15:23 Uhr

  • @Daniel, es gibt jetzt System-Lambda, das mit junit5 und Testing kompatibel ist.

    – MiB

    13. September 21 um 3:51 Uhr


Das Problem mit BufferedReader.readLine() ist, dass es sich um eine Sperrmethode handelt, die auf Benutzereingaben wartet. Es scheint mir, dass Sie das nicht besonders simulieren möchten (dh Sie möchten, dass Tests schnell sind). Aber in einem Testkontext kehrt es immer wieder zurück null mit hoher Geschwindigkeit beim Testen, was lästig ist.

Für einen Puristen kann man das machen getInputLine unten Paket-privat, und verspotten Sie es: easy-peezy.

String getInputLine() throws Exception {
    return br.readLine();
}

… müssten Sie sicherstellen, dass Sie eine Möglichkeit haben, (normalerweise) eine Schleife der Benutzerinteraktion mit der App zu stoppen. Sie müssten auch damit klarkommen, dass Ihre “Eingabeleitungen” immer gleich bleiben würden, bis Sie die irgendwie geändert haben doReturn Ihres Spotts: kaum typisch für Benutzereingaben.

Für einen Nicht-Puristen, der sich das Leben leicht machen möchte (und lesbare Tests erstellen möchte), könnten Sie all diese Dinge unten in Ihren App-Code einfügen:

private Deque<String> inputLinesDeque;

void setInputLines(List<String> inputLines) {
    inputLinesDeque = new ArrayDeque<String>(inputLines);
}

private String getInputLine() throws Exception {
    if (inputLinesDeque == null) {
        // ... i.e. normal case, during app run: this is then a blocking method
        return br.readLine();
    }
    String nextLine = null;
    try {
        nextLine = inputLinesDeque.pop();
    } catch (NoSuchElementException e) {
        // when the Deque runs dry the line returned is a "poison pill", 
        // signalling to the caller method that the input is finished
        return "q";
    }

    return nextLine;
}

… in deinem Test könntest du dann so vorgehen:

consoleHandler.setInputLines( Arrays.asList( new String[]{ "first input line", "second input line" }));

vor dem Auslösen der Methode in dieser “ConsoleHandler”-Klasse, die Eingabezeilen benötigt.

.

759330cookie-checkJUnit: Wie simuliert man System.in-Tests?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy