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ę.
java
eclipse
junit
user-input
Wimpey
źródło
źródło
Odpowiedzi:
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; }
źródło
\n
ale to nie robi różnicyByteArrayInputStream in = new ByteArrayInputStream(("1" + System.lineSeparator() + "2").getBytes());
do uzyskiwania wielu danych wejściowych dla różnychkeyboard.nextLine()
wywołań.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ć.
źródło
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.
źródło
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!
źródło
inputEntry = scanner.nextLine();
gdy jest używany zSystem.in
, zawsze będzie czekał na użytkownika (i zaakceptuje pustą linię jako pustąString
) ... podczas gdy gdy użyjesz linii, używającsystemInMock.provideLines()
tego, wyrzuci,NoSuchElementException
gdy skończy się linia. To naprawdę sprawia, że nie można zbytnio „zniekształcić” kodu aplikacji, aby umożliwić testowanie.systemInMock.provideLines("1", "2");
2. Używając System.setIn bez zewnętrznych bibliotek:String data2 = "1 2";
System.setIn(new ByteArrayInputStream(data2.getBytes()));
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.
źródło
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(); }
źródło
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.
źródło