2009-10-30 22 views
40

Mam program wiersza poleceń Java. Chciałbym stworzyć test JUnit, aby móc symulować System.in. Ponieważ kiedy mój program uruchomi się, dostanie się do pętli while i czeka na dane od użytkowników. Jak mogę to symulować w JUnit?JUnit: Jak symulować testowanie System.in?

Dzięki

Odpowiedz

49

Jest technicznie możliwe przełączanie System.in, ale ogólnie rzecz biorąc, bardziej odporne byłoby nie wywoływanie go bezpośrednio w kodzie, ale dodawanie warstwy pośredniej, aby źródło wejściowe było sterowane z jednego punktu w aplikacji. Dokładnie, jak to robisz, to szczegół implementacji - sugestie zastrzyku zależności są w porządku, ale niekoniecznie musisz wprowadzać frameworki zewnętrzne; na przykład można przekazać kontekst wejścia/wyjścia z kodu wywołującego.

Jak przełączyć 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); 
} 
1

Można by utworzyć niestandardowy InputStream i dołączyć go do klasy

class FakeInputStream extends InputStream { 

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

a następnie użyć go System ze swoją Scanner

System.in = new FakeInputStream();

Przed:

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

Po:

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

Chociaż myślę, że należy lepiej sprawdzić, jak klasa powinna pracować z danych odczytanych ze strumienia wejściowego, a nie jak to naprawdę brzmi stamtąd.

+0

Z perspektywy TDD, to unika się projektowania, że ​​test jest "jazda" lub próbie wskazania. Jednak PO nie określał TDD, a z perspektywy testowej jest to bardzo rozsądne - wykorzystać system globalny. – Yishai

+0

Nie można po prostu zrobić System.in = xxx, ponieważ System.in jest ostateczny. Możesz użyć System.setIn, ale upewnij się, że powrócisz do domyślnego przy zerwaniu. Ponadto nie trzeba uruchamiać własnego InputStream, ByteArrayInputStream będzie ładnie działać. –

+0

Oohh yeap, byłem zdezorientowany. Próbowałem powiedzieć, że ... cóż, zmienię mój wpis :) – OscarRyz

4

Spróbuj zmienić kod, aby użyć dependency injection. Zamiast posiadania metody, która bezpośrednio używa metody System.in, należy przyjąć jako argument metodę InputStream. Następnie w twoim teście na jedną, będziesz w stanie zdać test implementacji InputStream zamiast System.in.

7

Istnieje kilka sposobów podejścia do tego. Najbardziej kompletnym sposobem jest przekazanie w InputStream podczas uruchamiania testowanej klasy, która jest fałszywym InputStreamem, który przekazuje symulowane dane do twojej klasy. Możesz zajrzeć ram iniekcji zależność (takie jak Google Guice) jeśli trzeba to zrobić dużo w kodzie, ale prosty sposób to:

public class MyClass { 
    private InputStream systemIn; 

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

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

badanego nazwałbyś konstruktora, że ​​trwa wejście strumień. Chmura nawet czyni ten pakiet konstruktora prywatnym i umieszcza test w tym samym pakiecie, aby inny kod generalnie nie rozważał jego użycia.

+3

+1. Jestem z tobą w tej sprawie. Chciałbym pójść o krok dalej: 'InputData' jako opakowanie wysokiego poziomu wokół' InputStream' w testowaniu jednostkowym, powinieneś bardziej dbać o to, co robi twoja klasa, a nie tak naprawdę o integracji. – OscarRyz

+0

Zamiast tworzyć zmienną "systemIn" możesz użyć metody 'System.setIn (in)'. Możesz więc normalnie wywoływać 'System.in', ale ze zaktualizowaną wersją. – LaraChicharo

3

Możesz napisać jasną test dla interfejsu wiersza poleceń za pomocą TextFromStandardInputStream regułę biblioteki System Rules.

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

Pełne ujawnienie: Jestem autorem tej biblioteki.

+0

Po prostu próbowałem użyć twojej biblioteki, która wydawała się bardzo dobra ... ale potem próbowałem jej używać z Regułą publiczną MockitoRule rule = MockitoJUnit.rule() ;, i wydawało się, o ile próbowałem, że te dwie nie mogły być połączonym ... Więc nie byłem w stanie połączyć go z drwinami wstrzyknięć ... –

+0

Tak nie powinno być. Czy mógłbyś proszę utworzyć problem dla biblioteki reguł systemowych i podać swój nieudany przykład. –

+0

Dzięki ... teraz wiem, że to ma działać. Zajmę się tym i utworzę problem, jeśli dotyczy. Wydawało się, że jest to 'TextFromStandardInputStream', z którym miałem problem, ale' SystemOutRule' działało OK. –

1

Problem z BufferedReader.readLine() polega na tym, że jest to metoda blokująca, która czeka na wprowadzenie danych przez użytkownika.Wydaje mi się, że nie chcesz tego specjalnie symulować (tzn. Chcesz, aby testy były szybkie). Ale w kontekście testowym nieustannie zwraca on dużą prędkość podczas testu, co jest irytujące.

Dla purystów można ustawić getInputLine poniżej jako prywatny pakiet i wyśmiać go: easy-peezy.

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

... musisz się upewnić, że masz sposób zatrzymania (zwykle) pętli interakcji użytkownika z aplikacją. Musiałbyś także poradzić sobie z faktem, że twoje "linie wejściowe" będą zawsze takie same, dopóki nie zmienisz w jakiś sposób fałszywego: nietypowego dla użytkownika.

przypadku nieprzestrzegania purist, który chce ułatwić życie dla siebie (i produkują testy czytelny) można umieścić wszystkie te rzeczy poniżej w kodzie aplikacji:

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

... w teście to polubisz następnie wykonaj to:

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

przed wyzwoleniem metody w tej klasie "ConsoleHandler", która wymaga linii wejściowych.

0

może tak (nie testowane):

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

więcej części:

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