2009-10-30 14 views
40

Ho un programma da riga di comando Java. Vorrei creare un caso di test JUnit per poter simulare lo System.in. Perché quando il mio programma viene eseguito, entrerà nel ciclo while e attenderà input dagli utenti. Come faccio a simularlo in JUnit?JUnit: come simulare il test di System.in?

Grazie

risposta

49

È tecnicamente possibile cambiare System.in, ma in generale, sarebbe più sicuro non chiamarlo direttamente nel codice, ma aggiungere uno strato di riferimento indiretto in modo che la sorgente di input sia controllata da un punto nell'applicazione. Esattamente come lo si fa è un dettaglio dell'implementazione - i suggerimenti dell'iniezione di dipendenza vanno bene, ma non è necessariamente necessario introdurre framework di terze parti; Ad esempio, potresti passare un contesto di I/O dal codice chiamante.

Come passare 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

Si potrebbe creare un costume InputStream e allegarlo alla System classe

class FakeInputStream extends InputStream { 

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

e poi usarlo con il tuo Scanner

System.in = new FakeInputStream();

Prima:

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

Dopo:

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

Anche se credo che si dovrebbe meglio per verificare come la classe dovrebbe funzionare con i dati letti dal flusso di input e non realmente come si legge da lì.

+0

Dal punto di vista TDD, questo evita il disegno che il test è "guida" o il tentativo di indicare. Tuttavia, l'OP non ha specificato TDD e, dal punto di vista del test, è una cosa molto ragionevole da fare: sfruttare il sistema globale. – Yishai

+0

Non puoi semplicemente fare System.in = xxx come System.in è definitivo. È possibile utilizzare System.setIn, ma assicurarsi di tornare al valore predefinito nella rimozione. Inoltre, non è necessario eseguire il rollback del proprio InputStream, ByteArrayInputStream eseguirà il lavoro in modo corretto. –

+0

Oohh yeap, mi sono confuso. Quello che ho cercato di dire è stato ... beh, modificherò la mia voce :) – OscarRyz

4

Provare a rifasare il codice per utilizzare dependency injection. Invece di avere il tuo metodo che usa direttamente, il metodo accetta un argomento come InputStream. Quindi, nel test di junit, sarà possibile superare l'implementazione di prova InputStream al posto di System.in.

7

Ci sono alcuni modi per avvicinarsi a questo. Il modo più completo è passare in un InputStream mentre si esegue la classe in prova, che è un InputStream falso che trasmette dati simulati alla classe. Può consultare un quadro iniezione di dipendenza (come Google Guice) se avete bisogno di fare questo molto nel codice, ma il modo più semplice è:

public class MyClass { 
    private InputStream systemIn; 

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

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

in prova si sarebbe chiamare il costruttore che prende l'input ruscello. Il cloud rende persino privato il pacchetto del costruttore e inserisce il test nello stesso pacchetto, in modo che l'altro codice non prenda in considerazione l'idea di usarlo.

+3

+1. Sono con te su questo. Vorrei fare un passo ulteriore: 'InputData' come un elevato livello wrapper' InputStream' in unit testing, si dovrebbe preoccuparsi di più su ciò che la classe fa, e non realmente circa l'integrazione. – OscarRyz

+0

Invece di creare la variabile 'systemIn' è possibile utilizzare il metodo' System.setIn (in) '. Quindi puoi chiamare 'System.in' normalmente ma con la versione aggiornata. – LaraChicharo

3

È possibile scrivere un test chiaro per l'interfaccia della riga di comando utilizzando il TextFromStandardInputStream regola della biblioteca System Rules.

Full disclosure: Sono l'autore di quella libreria.

+0

Ho appena provato ad usare la tua libreria, che mi è sembrata molto buona ... ma poi ho provato a usarla con Rule public MockitoRule rule = MockitoJUnit.rule(); e sembrava, per quanto ho provato, che i due non potessero essere combinati ... Quindi io non sono riuscito a combinare con l'iniezione di mock ... –

+0

questo non dovrebbe essere il caso. Potresti creare un problema per la libreria Regole di sistema e fornire il tuo esempio non funzionante. –

+0

Grazie ... ora so che è destinato a lavorare, lo esaminerò e creerò un problema se applicabile. Sembrava che fosse il "TextFromStandardInputStream" con cui avevo un problema, ma "SystemOutRule" funzionava correttamente. –

1

Il problema con BufferedReader.readLine() è che si tratta di un metodo di blocco che attende l'input dell'utente.Mi sembra che tu non voglia particolarmente simularlo (cioè vuoi che i test siano veloci). Ma in un contesto di test ritorna continuamente null ad alta velocità durante il test, che è fastidioso.

Per un purista si può fare la getInputLine sotto package-privato, e prendere in giro è: facile-Peezy.

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

... dovresti assicurarti di avere un modo per fermare (in genere) un ciclo di interazione dell'utente con l'app. Faresti anche avere per far fronte al fatto che i vostri "linee di ingresso" sarebbe sempre lo stesso fino in qualche modo cambiato il doReturn del vostro finto: difficilmente tipico di input dell'utente.

per un non-purista che vuole rendere la vita facile per loro stessi (e produrre le prove leggibili) si potrebbe mettere tutta questa roba qui sotto nel codice dell'app:

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

... nel test si potrebbe quindi procedere come segue:

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

prima di attivare il metodo in questa classe "ConsoleHandler" che necessita di linee di input.

0

forse come questo (non testato):

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

più parti:

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