java - testsuite - JUnit: ¿Cómo simular las pruebas de System.in?
run unit test java command line (8)
Tengo un programa de línea de comandos de Java. Me gustaría crear el caso de prueba JUnit para poder simular System.in
. Porque cuando mi programa se ejecuta, entrará en el ciclo while y esperará la entrada de los usuarios. ¿Cómo simulo eso en JUnit?
Gracias
Basado en la respuesta de @ McDowell y en otra respuesta que muestra cómo probar System.out , me gustaría compartir mi solución para dar entrada a un programa y probar su resultado.
Como referencia, utilizo JUnit 4.12.
Digamos que tenemos este programa que simplemente replica la entrada al resultado:
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 probarlo, podemos usar la siguiente clase:
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());
}
}
No explicaré mucho, porque creo que el código es legible y cité mis fuentes.
Cuando JUnit ejecuta testCase1()
, llamará a los métodos auxiliares en el orden en que aparecen:
-
setUpOutput()
, debido a la anotación@Before
-
provideInput(String data)
, llamado desdetestCase1()
-
getOutput()
, llamado desdetestCase1()
-
restoreSystemInputOutput()
, debido a la anotación@After
No System.err
porque no lo necesitaba, pero debería ser fácil de implementar, similar a probar System.out
.
El problema con BufferedReader.readLine()
es que es un método de bloqueo que espera la entrada del usuario. Me parece que no desea particularmente simular eso (es decir, quiere que las pruebas sean rápidas). Pero en un contexto de prueba continuamente devuelve null
a alta velocidad durante la prueba, lo cual es molesto.
Para un purista, puede hacer que getInputLine
continuación, sea privado, y se burle: easy-peezy.
String getInputLine() throws Exception {
return br.readLine();
}
... tendrías que asegurarte de que tienes una forma de detener (típicamente) un bucle de interacción del usuario con la aplicación. También tendría que lidiar con el hecho de que sus "líneas de entrada" siempre serían las mismas hasta que de alguna manera haya cambiado el doReturn
de su simulacro: apenas típico de la entrada del usuario.
Para una persona no purista que desea hacerse la vida más fácil (y producir pruebas legibles), puede poner todo esto a continuación en su código de aplicación:
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;
}
... en su prueba, entonces podría ir así:
consoleHandler.setInputLines( Arrays.asList( new String[]{ "first input line", "second input line" }));
antes de activar el método en esta clase "ConsoleHandler" que necesita líneas de entrada.
Es técnicamente posible cambiar System.in
, pero, en general, sería más sólido no llamarlo directamente en el código, sino agregar una capa de indirección para que la fuente de entrada se controle desde un punto de la aplicación. Exactamente cómo lo hace es un detalle de implementación: las sugerencias de inyección de dependencia son buenas, pero no necesariamente es necesario introducir marcos de terceros; por ejemplo, podría pasar un contexto de E / S del código de llamada.
Cómo cambiar 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);
}
Hay algunas formas de abordar esto. La forma más completa es pasar un InputStream mientras se ejecuta la clase bajo prueba, que es un InputStream falso que pasa datos simulados a su clase. Puede ver un marco de inyección de dependencias (como Google Guice) si necesita hacer esto mucho en su código, pero la manera más simple es:
public class MyClass {
private InputStream systemIn;
public MyClass() {
this(System.in);
}
public MyClass(InputStream in) {
systemIn = in;
}
}
En prueba llamaría al constructor que toma la secuencia de entrada. Incluso puedes hacer que ese paquete constructor sea privado y poner la prueba en el mismo paquete, de modo que otro código generalmente no considere usarlo.
Intente refactorizar su código para usar la inyección de dependencia . En lugar de tener un método que utiliza System.in
directamente, System.in
que el método acepte un InputStream
como argumento. Luego, en su prueba junit, podrá aprobar una implementación InputStream
prueba en lugar de System.in
.
Puede crear un InputStream
personalizado y adjuntarlo a la clase System
class FakeInputStream extends InputStream {
public int read() {
return -1;
}
}
Y luego usarlo con su Scanner
System.in = new FakeInputStream();
Antes de:
InputStream in = System.in;
...
Scanner scanner = new Scanner( in );
Después:
InputStream in = new FakeInputStream();
...
Scanner scanner = new Scanner( in );
Aunque creo que es mejor que pruebes cómo debería funcionar tu clase con los datos leídos del flujo de entrada y no cómo se lee desde allí.
Puede escribir una prueba clara para la interfaz de línea de comando utilizando la regla TextFromStandardInputStream
de la biblioteca de reglas del 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());
}
}
Divulgación completa: soy el autor de esa biblioteca.
tal como esto (no probado):
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 );
más 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);