JUnit-Test mit simulierter Benutzereingabe


81

Ich versuche, einige JUnit-Tests für eine Methode zu erstellen, für die Benutzereingaben erforderlich sind. Die zu testende Methode sieht ungefähr wie folgt aus:

public static int testUserInput() {
    Scanner keyboard = new Scanner(System.in);
    System.out.println("Give a number between 1 and 10");
    int input = keyboard.nextInt();

    while (input < 1 || input > 10) {
        System.out.println("Wrong number, try again.");
        input = keyboard.nextInt();
    }

    return input;
}

Gibt es eine Möglichkeit, das Programm automatisch an ein int zu übergeben, anstatt dass ich oder jemand anderes dies manuell in der JUnit-Testmethode tut? Möchten Sie die Benutzereingaben simulieren?

Danke im Voraus.


7
Was genau testest du? Scanner? Eine Testmethode sollte normalerweise etwas Nützliches bestätigen.
Markus

2
Sie sollten die Scannerklasse von Java nicht testen müssen. Sie können Ihre Eingabe manuell einstellen und einfach Ihre eigene Logik testen. int input = -1 oder 5 oder 11 wird Ihre Logik
abdecken

3
Sechs Jahre später ... und es ist immer noch eine gute Frage. Nicht zuletzt, weil Sie zu Beginn der Entwicklung einer App normalerweise nicht alle Schnickschnack von JavaFX haben möchten, sondern einfach die bescheidene Befehlszeile für eine sehr grundlegende Interaktion verwenden möchten. Schade, dass JUnit das nicht ganz einfacher macht. Für mich ist die Antwort von Omar Elshal sehr nett, mit minimaler "erfundener" oder "verzerrter" App-Codierung ...
Mike Nagetier

Antworten:


103

Sie können System.in durch Ihren eigenen Stream ersetzen, indem Sie System.setIn (InputStream in) aufrufen . InputStream kann ein Byte-Array sein:

InputStream sysInBackup = System.in; // backup System.in to restore it later
ByteArrayInputStream in = new ByteArrayInputStream("My string".getBytes());
System.setIn(in);

// do your thing

// optionally, reset System.in to its original
System.setIn(sysInBackup);

Ein anderer Ansatz kann diese Methode testbarer machen, indem IN und OUT als Parameter übergeben werden:

public static int testUserInput(InputStream in,PrintStream out) {
    Scanner keyboard = new Scanner(in);
    out.println("Give a number between 1 and 10");
    int input = keyboard.nextInt();

    while (input < 1 || input > 10) {
        out.println("Wrong number, try again.");
        input = keyboard.nextInt();
    }

    return input;
}

5
@KrzyH Wie würde ich das mit mehreren Eingaben machen? Sagen Sie, dass ich in // do your thing drei Eingabeaufforderungen zur Benutzereingabe habe. Wie würde ich das machen?
Stupid.Fat.Cat

1
@ Stupid.Fat.Cat Ich habe zweiten Ansatz zum Problem hinzugefügt, eleganter und felxible
KrzyH

1
@KrzyH Für den zweiten Ansatz müssen Sie den Eingabestream und den Ausgabestream als Parameter in der Methodendefinition übergeben, was ich nicht suche. Kennen Sie einen besseren Weg?
Chirag

6
Wie simuliere ich das Drücken der Eingabetaste? Im Moment liest das Programm nur alle Eingaben auf einmal, was nicht gut ist. Ich habe es versucht, \naber das macht keinen Unterschied
CodyBugstein

3
@CodyBugstein Verwenden Sie diese Option ByteArrayInputStream in = new ByteArrayInputStream(("1" + System.lineSeparator() + "2").getBytes());, um mehrere Eingaben für verschiedene keyboard.nextLine()Anrufe abzurufen .
Ein Glas Ton

18

Um Ihren Code zu testen, sollten Sie einen Wrapper für Systemeingabe- / Ausgabefunktionen erstellen. Sie können dies mithilfe der Abhängigkeitsinjektion tun und uns eine Klasse geben, die nach neuen Ganzzahlen fragen kann:

public static class IntegerAsker {
    private final Scanner scanner;
    private final PrintStream out;

    public IntegerAsker(InputStream in, PrintStream out) {
        scanner = new Scanner(in);
        this.out = out;
    }

    public int ask(String message) {
        out.println(message);
        return scanner.nextInt();
    }
}

Dann können Sie Tests für Ihre Funktion erstellen, indem Sie ein Mock-Framework verwenden (ich verwende Mockito):

@Test
public void getsIntegerWhenWithinBoundsOfOneToTen() throws Exception {
    IntegerAsker asker = mock(IntegerAsker.class);
    when(asker.ask(anyString())).thenReturn(3);

    assertEquals(getBoundIntegerFromUser(asker), 3);
}

@Test
public void asksForNewIntegerWhenOutsideBoundsOfOneToTen() throws Exception {
    IntegerAsker asker = mock(IntegerAsker.class);
    when(asker.ask("Give a number between 1 and 10")).thenReturn(99);
    when(asker.ask("Wrong number, try again.")).thenReturn(3);

    getBoundIntegerFromUser(asker);

    verify(asker).ask("Wrong number, try again.");
}

Schreiben Sie dann Ihre Funktion, die die Tests besteht. Die Funktion ist viel sauberer, da Sie die Duplizierung von Abfragen / Abrufen von Ganzzahlen entfernen können und die tatsächlichen Systemaufrufe gekapselt sind.

public static void main(String[] args) {
    getBoundIntegerFromUser(new IntegerAsker(System.in, System.out));
}

public static int getBoundIntegerFromUser(IntegerAsker asker) {
    int input = asker.ask("Give a number between 1 and 10");
    while (input < 1 || input > 10)
        input = asker.ask("Wrong number, try again.");
    return input;
}

Dies mag für Ihr kleines Beispiel wie ein Overkill erscheinen, aber wenn Sie eine größere Anwendung erstellen, kann sich eine solche Entwicklung ziemlich schnell auszahlen.


5

Eine übliche Methode zum Testen eines ähnlichen Codes besteht darin, eine Methode zu extrahieren, die einen Scanner und einen PrintWriter enthält, ähnlich wie bei dieser StackOverflow-Antwort , und Folgendes zu testen:

public void processUserInput() {
  processUserInput(new Scanner(System.in), System.out);
}

/** For testing. Package-private if possible. */
public void processUserInput(Scanner scanner, PrintWriter output) {
  output.println("Give a number between 1 and 10");
  int input = scanner.nextInt();

  while (input < 1 || input > 10) {
    output.println("Wrong number, try again.");
    input = scanner.nextInt();
  }

  return input;
}

Beachten Sie, dass Sie Ihre Ausgabe erst am Ende lesen können und alle Ihre Eingaben im Voraus angeben müssen:

@Test
public void shouldProcessUserInput() {
  StringWriter output = new StringWriter();
  String input = "11\n"       // "Wrong number, try again."
               + "10\n";

  assertEquals(10, systemUnderTest.processUserInput(
      new Scanner(input), new PrintWriter(output)));

  assertThat(output.toString(), contains("Wrong number, try again.")););
}

Anstatt eine Überladungsmethode zu erstellen, können Sie natürlich auch "Scanner" und "Ausgabe" als veränderbare Felder in Ihrem zu testenden System beibehalten. Ich mag es, den Unterricht so staatenlos wie möglich zu halten, aber das ist kein großes Zugeständnis, wenn es Ihnen oder Ihren Mitarbeitern / Ausbildern wichtig ist.

Sie können Ihren Testcode auch in dasselbe Java-Paket wie den zu testenden Code einfügen (auch wenn er sich in einem anderen Quellordner befindet), wodurch Sie die Sichtbarkeit der Überladung mit zwei Parametern lockern können, um paketprivat zu sein.


Sie wollten zum Testen, dass die Methode processUserInput () die Methode (in, out) und nicht processUserInput (in, out) aufruft?
NadZ

@ NadZ Natürlich; Ich habe mit einem allgemeinen Beispiel begonnen und es unvollständig wieder so geändert, dass es spezifisch für die Frage ist.
Jeff Bowman

3

Ich habe es geschafft, einen einfacheren Weg zu finden. Sie müssen jedoch die externe Bibliothek System.rules von @Stefan Birkner verwenden

Ich habe nur das dort bereitgestellte Beispiel genommen, ich denke, es hätte nicht einfacher sein können:

import java.util.Scanner;

public class Summarize {
  public static int sumOfNumbersFromSystemIn() {
    Scanner scanner = new Scanner(System.in);
    int firstSummand = scanner.nextInt();
    int secondSummand = scanner.nextInt();
    return firstSummand + secondSummand;
  }
}

Prüfung

import static org.junit.Assert.*;
import static org.junit.contrib.java.lang.system.TextFromStandardInputStream.*;

import org.junit.Rule;
import org.junit.Test;
import org.junit.contrib.java.lang.system.TextFromStandardInputStream;

public class SummarizeTest {
  @Rule
  public final TextFromStandardInputStream systemInMock
    = emptyStandardInputStream();

  @Test
  public void summarizesTwoNumbers() {
    systemInMock.provideLines("1", "2");
    assertEquals(3, Summarize.sumOfNumbersFromSystemIn());
  }
}

Das Problem ist jedoch in meinem Fall, dass meine zweite Eingabe Leerzeichen enthält und der gesamte Eingabestream null ist!


Das funktioniert sehr gut ... Ich weiß nicht, was Sie unter "Eingabestream null" verstehen. Was gut ist, ist, dass inputEntry = scanner.nextLine();bei Verwendung mit System.inimmer auf den Benutzer gewartet wird (und eine leere Zeile als leer akzeptiert wird String) ... wohingegen, wenn Sie Leitungen verwenden, systemInMock.provideLines()dies verwendet wird, NoSuchElementExceptionwenn die Zeilen ausgehen. Dies macht es wirklich einfach, den App-Code nicht zu stark zu "verzerren", um den Tests gerecht zu werden.
Mike Nagetier

Ich erinnere mich nicht genau, was das Problem war, aber Sie haben Recht, ich habe meinen Code erneut überprüft und festgestellt, dass ich ihn auf zwei Arten behoben habe: 1. Verwenden von systemInMock: systemInMock.provideLines("1", "2"); 2. Verwenden von System.setIn ohne externe Bibliotheken:String data2 = "1 2"; System.setIn(new ByteArrayInputStream(data2.getBytes()));
Omar Elshal

2

Sie können beginnen, indem Sie die Logik, die die Nummer von der Tastatur abruft, in eine eigene Methode extrahieren. Anschließend können Sie die Validierungslogik testen, ohne sich um die Tastatur kümmern zu müssen. Um den Aufruf von keyboard.nextInt () zu testen, sollten Sie ein Scheinobjekt verwenden.


2
Beachten Sie, dass Sie Scannerobjekte nicht verspotten können (sie sind endgültig).
Hoipolloi

2

Ich habe das Problem beim Lesen von stdin behoben, um eine Konsole zu simulieren ...

Meine Probleme waren, ich würde gerne versuchen, in JUnit die Konsole zu testen, um ein bestimmtes Objekt zu erstellen ...

Das Problem ist wie alles, was Sie sagen: Wie kann ich im Stdin vom JUnit-Test schreiben?

Dann lerne ich am College etwas über Weiterleitungen, wie Sie sagen, System.setIn (InputStream) ändert den Standard-Dateiskriptor und Sie können dann schreiben ...

Aber es gibt noch ein Problem zu beheben ... den JUnit-Testblock, der darauf wartet, von Ihrem neuen InputStream gelesen zu werden. Sie müssen also einen Thread erstellen, der aus dem InputStream und aus dem JUnit-Test-Thread gelesen werden kann. Schreiben Sie in den neuen Stdin ... Zuerst müssen Sie Schreiben Sie in das Stdin, denn wenn Sie später schreiben oder den Thread erstellen, um aus dem Stdin zu lesen, haben Sie wahrscheinlich Race-Bedingungen ... Sie können vor dem Lesen in den InputStream schreiben oder vor dem Schreiben aus dem InputStream lesen ...

Dies ist mein Code, meine Englischkenntnisse sind schlecht. Ich hoffe, Sie können das Problem und die Lösung für die Simulation des Schreibens in stdin aus dem JUnit-Test verstehen.

private void readFromConsole(String data) throws InterruptedException {
    System.setIn(new ByteArrayInputStream(data.getBytes()));

    Thread rC = new Thread() {
        @Override
        public void run() {
            study = new Study();
            study.read(System.in);
        }
    };
    rC.start();
    rC.join();      
}

1

Ich fand es hilfreich, eine Schnittstelle zu erstellen, die Methoden definiert, die java.io.Console ähneln, und diese dann zum Lesen oder Schreiben in System.out zu verwenden. Die eigentliche Implementierung wird an System.console () delegiert, während Ihre JUnit-Version ein Scheinobjekt mit vordefinierten Eingaben und erwarteten Antworten sein kann.

Beispielsweise würden Sie eine MockConsole erstellen, die die vordefinierten Eingaben des Benutzers enthält. Die Scheinimplementierung würde bei jedem Aufruf von readLine eine Eingabezeichenfolge aus der Liste streichen. Es würde auch die gesamte Ausgabe sammeln, die in eine Liste von Antworten geschrieben wurde. Wenn am Ende des Tests alles gut gegangen wäre, wäre Ihre gesamte Eingabe gelesen worden, und Sie können die Ausgabe bestätigen.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.