wake-up-neo.com

JUnit: Wie simuliert man System.in-Tests?

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

Vielen Dank

47
Noppanit

Es ist technisch möglich, System.in umzuschalten. Im Allgemeinen wäre es jedoch robuster, es nicht direkt in Ihrem Code aufzurufen, sondern eine Ebene der Indirektion hinzuzufügen, sodass die Eingabequelle von einem Punkt in Ihrer Anwendung gesteuert wird. Genau wie Sie dies tun, ist ein Implementierungsdetail. Die Vorschläge für die Abhängigkeitsinjektion sind in Ordnung, aber Sie müssen nicht unbedingt Frameworks von Drittanbietern einführen. Sie könnten beispielsweise einen E/A-Kontext aus dem aufrufenden Code übergeben.

So wechseln Sie System.in:

String data = "Hello, World!\r\n";
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);
}
59
McDowell

Es gibt einige Wege, um dies zu erreichen. Die vollständigste Methode besteht darin, einen InputStream zu übergeben, während die zu testende Klasse ausgeführt wird. Dabei handelt es sich um einen gefälschten InputStream, der simulierte Daten an Ihre Klasse weitergibt. Sie können ein Abhängigkeitsinjektions-Framework (z. B. Google Guice) anzeigen, wenn Sie dies in Ihrem Code häufig tun müssen. Dies ist jedoch der einfache Weg:

 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 übernimmt. In der Cloud wird das Konstruktorpaket sogar als privat deklariert und der Test wird in demselben Paket abgelegt, sodass der Code in der Regel nicht in Betracht gezogen wird.

8
Yishai

Basierend auf @ McDowells Antwort und einer weiteren Antwort, die zeigt, wie System.out getestet wird , möchte ich meine Lösung mitteilen, 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 die Eingabe einfach 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 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 testCase1() ausführt, werden die Hilfemethoden in der Reihenfolge aufgerufen, in der sie angezeigt werden:

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

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

Versuchen Sie, Ihren Code umzuwandeln, um die Abhängigkeitsinjektion zu verwenden. Lassen Sie statt einer Methode, die System.in direkt verwendet, eine InputStream als Argument akzeptieren. In Ihrem Junit-Test können Sie anstelle von System.in eine Implementierung von test InputStream bestehen. 

4
Asaph

Sie können einen klaren Test für die Befehlszeilenschnittstelle schreiben, indem Sie die TextFromStandardInputStream-Regel der Bibliothek System Rules verwenden.

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.

4
Stefan Birkner

Sie können ein benutzerdefiniertes InputStream erstellen. und fügen Sie es der Klasse System hinzu

class FakeInputStream extends InputStream {

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

Und verwenden Sie es dann 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 Eingabestream gelesenen Daten arbeiten soll und nicht wirklich, wie sie von dort gelesen werden.

1
OscarRyz

Das Problem bei BufferedReader.readLine() ist, dass es sich um eine Blockierungsmethode handelt, die auf Benutzereingaben wartet. Es scheint mir, dass Sie das nicht unbedingt simulieren wollen (d. H. Sie möchten, dass Tests schnell sind). In einem Testkontext gibt es jedoch während des Tests ständig null zurück, was ärgerlich ist.

Für einen Puristen können Sie die getInputLine unter package-private machen und es verspotten: easy-peezy.

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

... Sie müssen sicherstellen, dass Sie eine Möglichkeit haben, eine Schleife der Benutzerinteraktion mit der App (normalerweise) zu stoppen. Sie müssen sich auch damit auseinandersetzen, dass Ihre "Eingabezeilen" immer gleich sind, bis Sie die doReturn Ihres Mock irgendwie geändert haben: kaum typisch für Benutzereingaben.

Für einen Nicht-Puristen, der sich das Leben leicht machen und lesbare Tests erstellen möchte, könnten Sie all das 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 Ihrem Test könnten Sie dann so gehen:

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.

1
mike rodent

vielleicht so (nicht getestet):

InputStream save_in=System.in;final PipedOutputStream in = new PipedOutputStream(); System.setIn(new PipedInputStream(in));

in.write("text".getBytes("utf-8"));

System.setIn( save_in );

mehr Teile:

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

InputStream save_in=System.in;final PipedOutputStream in = new PipedOutputStream(); System.setIn(new PipedInputStream(in));

//start something that reads stdin probably in a new thread
//  Thread thread=new Thread(new Runnable() {
//      @Override
//      public void run() {
//          CoursesApiApp.main(new String[]{});                 
//      }
//  });
//  thread.start();


//maybe wait or read the output
//  for(int limit=0; limit<60 && not_ready ; limit++)
//  {
//      try {
//          Thread.sleep(100);
//      } catch (InterruptedException e) {
//          e.printStackTrace();
//      }
//  }


in.write("text".getBytes("utf-8"));

System.setIn( save_in );

//System.setOut(save_out);
0
Shimon Doodkin