JUnit: Como simular o teste System.in?

Eu tenho um programa de linha de comando Java. Eu gostaria de criar caso de teste JUnit para ser capaz de simular System.in . Porque quando meu programa for executado, ele entrará no loop while e aguardará a input dos usuários. Como faço para simular isso no JUnit?

obrigado

É tecnicamente possível alternar o System.in , mas, em geral, seria mais robusto não chamá-lo diretamente em seu código, mas adicionar uma camada de indireto para que a origem de input seja controlada a partir de um ponto em seu aplicativo. Exatamente como você faz isso é um detalhe de implementação – as sugestões de injeção de dependência são boas, mas você não precisa necessariamente apresentar estruturas de terceiros; você poderia passar um contexto de E / S do código de chamada, por exemplo.

Como mudar o 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); } 

Existem algumas maneiras de abordar isso. A maneira mais completa é passar em um InputStream enquanto executa a class em teste, que é um InputStream falso que transmite dados simulados para sua class. Você pode olhar para uma estrutura de injeção de dependência (como o Google Guice) se precisar fazer muito isso em seu código, mas a maneira mais simples é:

  public class MyClass { private InputStream systemIn; public MyClass() { this(System.in); } public MyClass(InputStream in) { systemIn = in; } } 

Em teste, você chamaria o construtor que recebe o stream de input. Você pode até tornar esse pacote de construtor privado e colocar o teste no mesmo pacote, para que outro código geralmente não considere usá-lo.

Tente refatorar seu código para usar a injeção de dependência . Em vez de ter um método que use System.in diretamente, faça o método aceitar um InputStream como um argumento. Em seguida, no seu teste junit, você poderá passar por uma implementação do InputStream teste no lugar de System.in .

Você pode escrever um teste claro para a interface de linha de comando usando a regra TextFromStandardInputStream da biblioteca de Regras do Sistema .

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

Divulgação completa: sou o autor dessa biblioteca.

Com base na resposta de @ McDowell e em outra resposta que mostra como testar o System.out , gostaria de compartilhar minha solução para fornecer uma input a um programa e testar sua saída.

Como referência, eu uso o JUnit 4.12.

Digamos que temos esse programa que simplesmente replica input para saída:

 import java.util.Scanner; public class SimpleProgram { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.print(scanner.next()); scanner.close(); } } 

Para testá-lo, podemos usar a seguinte class:

 import static org.junit.Assert.*; import java.io.*; import org.junit.*; public class SimpleProgramTest { private final InputStream systemIn = System.in; private final PrintStream systemOut = System.out; private ByteArrayInputStream testIn; private ByteArrayOutputStream testOut; @Before public void setUpOutput() { testOut = new ByteArrayOutputStream(); System.setOut(new PrintStream(testOut)); } private void provideInput(String data) { testIn = new ByteArrayInputStream(data.getBytes()); System.setIn(testIn); } private String getOutput() { return testOut.toString(); } @After public void restoreSystemInputOutput() { System.setIn(systemIn); System.setOut(systemOut); } @Test public void testCase1() { final String testString = "Hello!"; provideInput(testString); SimpleProgram.main(new String[0]); assertEquals(testString, getOutput()); } } 

Não vou explicar muito, porque acredito que o código é legível e citei minhas fonts.

Quando o JUnit executa o testCase1() , ele chama os methods auxiliares na ordem em que aparecem:

  1. setUpOutput() , por causa da anotação @Before
  2. provideInput(String data) , chamado de testCase1()
  3. getOutput() , chamado de testCase1()
  4. restoreSystemInputOutput() , por causa da anotação @After

Eu não testei o System.err porque não System.err dele, mas ele deve ser fácil de implementar, semelhante ao teste do System.out .

Você poderia criar um InputStream personalizado e anexá-lo à class System

 class FakeInputStream extends InputStream { public int read() { return -1; } } 

E, em seguida, use-o com seu Scanner

System.in = new FakeInputStream();

Antes:

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

Depois de:

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

Embora eu ache que você deveria testar melhor como sua class deve trabalhar com os dados lidos a partir do stream de input e não exatamente como ele lê a partir daí.

O problema com BufferedReader.readLine() é que é um método de bloqueio que aguarda a input do usuário. Parece-me que você não quer particularmente simular isso (ou seja, você quer que os testes sejam rápidos). Mas, em um contexto de testes, ele retorna continuamente null em alta velocidade durante os testes, o que é cansativo.

Para um purista, você pode tornar o getInputLine abaixo do package-private e ridicularizá-lo: easy-peezy.

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

… você teria que ter certeza de que tinha uma maneira de interromper (normalmente) um loop de interação do usuário com o aplicativo. Você também teria que lidar com o fato de que suas “linhas de input” sempre seriam as mesmas até que você de alguma forma alterasse o doReturn do seu mock: dificilmente típico de input do usuário.

Para um não-purista que deseja tornar a vida fácil para si (e produzir testes legíveis) você pode colocar tudo isso abaixo no código do aplicativo:

 private Deque inputLinesDeque; void setInputLines(List inputLines) { inputLinesDeque = new ArrayDeque(inputLines); } private String getInputLine() throws Exception { if (inputLinesDeque == null) { // ... ie 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; } 

… no seu teste você pode então ir assim:

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

antes de triggersr o método nesta class “ConsoleHandler” que precisa de linhas de input.

talvez assim (não testado):

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

mais partes:

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