Testowanie JUnit z symulowanym wejściem użytkownika


82

Próbuję utworzyć kilka testów JUnit dla metody, która wymaga danych wejściowych użytkownika. Testowana metoda wygląda trochę jak następująca metoda:

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

Czy jest możliwy sposób, aby automatycznie przekazać programowi int zamiast ja lub ktoś inny robi to ręcznie w metodzie testowej JUnit? Lubisz symulować dane wejściowe użytkownika?

Z góry dziękuję.


7
Co dokładnie testujesz? Skaner? Metoda testowa powinna zazwyczaj stwierdzać, że coś jest przydatne.
Markus

2
Nie powinieneś testować klasy skanera w Javie. Możesz ręcznie ustawić dane wejściowe i po prostu przetestować własną logikę. int input = -1 lub 5 lub 11 obejmie twoją logikę
blong824

3
Sześć lat później ... i nadal jest to dobre pytanie. Nie tylko dlatego, że rozpoczynając tworzenie aplikacji, zwykle nie chcesz mieć wszystkich dzwonków i gwizdków JavaFX, ale zamiast tego po prostu zacznij używać skromnego wiersza poleceń, aby uzyskać trochę bardzo podstawowej interakcji. Szkoda, że ​​JUnit nie ułatwia tego. Dla mnie odpowiedź Omara Elshala jest bardzo przyjemna, z minimalnym „wymyślonym” lub „zniekształconym” kodowaniem aplikacji ...
mike gryzoń

Odpowiedzi:


105

Możesz zamienić System.in na swój własny strumień, wywołując System.setIn (InputStream in) . InputStream może być tablicą bajtów:

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

Inne podejście może uczynić tę metodę bardziej testowalną, przekazując IN i OUT jako parametry:

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

6
@KrzyH jak bym to zrobił z wieloma wejściami? Powiedz, że w // rób swoje mam trzy monity o wprowadzenie danych przez użytkownika. Jak bym to zrobił?
Stupid.Fat.Cat

1
@ Stupid.Fat.Cat Dodałem drugie podejście do problemu, bardziej eleganckie i elastyczniejsze
KrzyH

1
@KrzyH W drugim podejściu będziesz musiał podać strumień wejściowy i strumień wyjściowy jako parametr w definicji metody, czego nie szukam. Czy znasz lepszy sposób?
Chirag

6
Jak zasymulować naciśnięcie klawisza Enter? W tej chwili program po prostu odczytuje wszystkie dane wejściowe za jednym razem, co nie jest dobre. Próbowałem, \nale to nie robi różnicy
CodyBugstein

3
@CodyBugstein Służy ByteArrayInputStream in = new ByteArrayInputStream(("1" + System.lineSeparator() + "2").getBytes());do uzyskiwania wielu danych wejściowych dla różnych keyboard.nextLine()wywołań.
A Jar of Clay

18

Aby przetestować swój kod, powinieneś utworzyć opakowanie dla funkcji wejścia / wyjścia systemu. Możesz to zrobić za pomocą iniekcji zależności, dając nam klasę, która może prosić o nowe liczby całkowite:

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

Następnie możesz stworzyć testy dla swojej funkcji, używając makiety (ja używam 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.");
}

Następnie napisz swoją funkcję, która przejdzie testy. Funkcja jest znacznie czystsza, ponieważ można usunąć powielanie liczby całkowitej z pytaniem / pobieraniem, a rzeczywiste wywołania systemowe są hermetyzowane.

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

Może się to wydawać przesadą dla twojego małego przykładu, ale jeśli tworzysz większą aplikację, rozwijanie w ten sposób może się dość szybko opłacić.


6

Jednym z powszechnych sposobów testowania podobnego kodu byłoby wyodrębnienie metody pobierającej Scanner i PrintWriter, podobnej do tej odpowiedzi StackOverflow , i sprawdzenie, czy:

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

Zwróć uwagę, że nie będziesz w stanie odczytać wyników do końca i będziesz musiał określić wszystkie swoje dane wejściowe z góry:

@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.")););
}

Oczywiście, zamiast tworzyć metodę przeciążania, można również testować „skaner” i „wyjście” jako zmienne pola w systemie. Lubię utrzymywać zajęcia tak bezpaństwowe, jak to tylko możliwe, ale nie jest to zbyt duże ustępstwo, jeśli ma to znaczenie dla ciebie lub twoich współpracowników / instruktorów.

Możesz również zdecydować się na umieszczenie kodu testowego w tym samym pakiecie Java, co testowany kod (nawet jeśli znajduje się w innym folderze źródłowym), co pozwoli ci zmniejszyć widoczność przeciążenia dwóch parametrów, aby zachować prywatność pakietu.


Czy chodziło Ci o testowanie, metoda processUserInput () wywołuje metodę (in, out), a nie processUserInput (in, out)?
NadZ

@NadZ Oczywiście; Zacząłem od ogólnego przykładu i nie do końca zmieniłem go z powrotem na specyficzny dla pytania.
Jeff Bowman,

4

Udało mi się znaleźć prostszy sposób. Musisz jednak użyć zewnętrznej biblioteki System.rules autorstwa @Stefan Birkner

Właśnie wziąłem podany tam przykład, myślę, że nie mogło być prostsze:

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

Test

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

Problem jednak w moim przypadku moje drugie wejście ma spacje i to sprawia, że ​​cały strumień wejściowy jest zerowy!


To działa bardzo dobrze… Nie wiem, co masz na myśli, mówiąc „strumień wejściowy zerowy”. Dobre jest to, że inputEntry = scanner.nextLine();gdy jest używany z System.in, zawsze będzie czekał na użytkownika (i zaakceptuje pustą linię jako pustą String) ... podczas gdy gdy użyjesz linii, używając systemInMock.provideLines()tego, wyrzuci, NoSuchElementExceptiongdy skończy się linia. To naprawdę sprawia, że ​​nie można zbytnio „zniekształcić” kodu aplikacji, aby umożliwić testowanie.
mike gryzoń

Nie pamiętam dokładnie, w czym był problem, ale masz rację, ponownie sprawdziłem swój kod i zauważyłem, że naprawiłem go na dwa sposoby: 1. Używając systemInMock: systemInMock.provideLines("1", "2"); 2. Używając System.setIn bez zewnętrznych bibliotek:String data2 = "1 2"; System.setIn(new ByteArrayInputStream(data2.getBytes()));
Omar Elshal

2

Możesz zacząć od wyodrębnienia logiki, która pobiera liczbę z klawiatury do własnej metody. Następnie możesz przetestować logikę walidacji bez martwienia się o klawiaturę. Aby przetestować wywołanie keyboard.nextInt (), możesz rozważyć użycie obiektu pozorowanego.


2
Uwaga, nie można kpić z obiektów skanera (są one ostateczne).
hoipolloi

2

Naprawiłem problem z odczytem ze stdin do symulacji konsoli ...

Moje problemy polegały na tym, że chciałbym spróbować napisać w JUnit, przetestować konsolę, aby utworzyć określony obiekt ...

Problem jest taki, jak wszystko, co mówisz: Jak mogę pisać w teście Stdin z JUnit?

Następnie na studiach dowiaduję się o przekierowaniach, takich jak mówisz System.setIn (InputStream), zmieniaj stdin filedescriptor i możesz pisać ...

Ale jest jeszcze jeden problem do naprawienia ... blok testowy JUnit czeka na odczytanie z nowego InputStream, więc musisz utworzyć wątek do odczytu z InputStream i z testu JUnit Zapis wątku w nowym standardowym standardzie ... Najpierw musisz napisz w Stdin, ponieważ jeśli napiszesz później o utworzeniu wątku do odczytu ze standardowego wejścia, prawdopodobnie będziesz mieć warunki wyścigu ... możesz napisać w InputStream przed odczytem lub możesz czytać z InputStream przed zapisem ...

To jest mój kod, moja znajomość angielskiego jest słaba. Mam nadzieję, że wszyscy rozumieją problem i rozwiązanie do symulacji zapisu w stdin z testu JUnit.

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

Uważam, że pomocne jest utworzenie interfejsu, który definiuje metody podobne do java.io.Console, a następnie używanie go do odczytu lub zapisu w System.out. Rzeczywista implementacja zostanie przekazana do System.console (), podczas gdy wersja JUnit może być obiektem pozorowanym z gotowymi danymi wejściowymi i oczekiwanymi odpowiedziami.

Na przykład można skonstruować konsolę MockConsole, która zawierałaby dane wejściowe użytkownika. Mock implementacja usuwała ciąg wejściowy z listy za każdym razem, gdy wywoływano readLine. Zbierałby również wszystkie dane wyjściowe zapisane na liście odpowiedzi. Gdyby pod koniec testu wszystko poszło dobrze, wszystkie dane wejściowe zostałyby odczytane i można by potwierdzić dane wyjściowe.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.