Prueba de JUnit con entrada de usuario simulada

81

Estoy tratando de crear algunas pruebas JUnit para un método que requiere la entrada del usuario. El método bajo prueba se parece un poco al método siguiente:

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

¿Hay alguna forma posible de pasar automáticamente al programa un int en lugar de que yo o alguien más lo haga manualmente en el método de prueba JUnit? ¿Te gusta simular la entrada del usuario?

Gracias por adelantado.

Wimpey
fuente
7
¿Qué estás probando exactamente? ¿Escáner? Normalmente, un método de prueba debe afirmar que algo es útil.
Markus
2
No debería tener que probar la clase Scanner de Java. Puede configurar manualmente su entrada y simplemente probar su propia lógica. int input = -1 o 5 u 11 cubrirá su lógica
blong824
3
Seis años después ... y sigue siendo una buena pregunta. Sobre todo porque al comenzar a desarrollar una aplicación, es posible que no desee tener todas las comodidades de JavaFX, sino que simplemente comience a usar la humilde línea de comandos para un poco de interacción muy básica. Es una pena que JUnit no facilite esto un poco. Para mí, la respuesta de Omar Elshal es muy agradable, con una mínima codificación de aplicaciones "artificial" o "distorsionada" involucrada ...
Mike Rodent

Respuestas:

103

Puede reemplazar System.in con su propia secuencia llamando a System.setIn (InputStream in) . InputStream puede ser una matriz de bytes:

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

Un enfoque diferente puede hacer que este método sea más comprobable pasando IN y OUT como parámetros:

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;
}
KrzyH
fuente
5
@KrzyH, ¿cómo haría esto con múltiples entradas? Di que en // haz lo tuyo, tengo tres mensajes para la entrada del usuario. ¿Cómo haría eso?
Stupid.Fat.Cat
1
@ Stupid.Fat.Cat He agregado un segundo enfoque al problema, más elegante y felxible
KrzyH
1
@KrzyH Para el segundo enfoque, se le pedirá que pase el flujo de entrada y el flujo de salida como un parámetro en la definición del método, que es lo que no estoy buscando. ¿Conoces alguna forma mejor?
Chirag
6
¿Cómo simulo presionar la tecla enter? En este momento, el programa simplemente lee toda la entrada de una sola vez, lo que no es bueno. Lo intenté \npero eso no hace la diferencia
CodyBugstein
3
@CodyBugstein Úselo ByteArrayInputStream in = new ByteArrayInputStream(("1" + System.lineSeparator() + "2").getBytes());para obtener múltiples entradas para diferentes keyboard.nextLine()llamadas.
A Jar of Clay
18

Para probar su código, debe crear un contenedor para las funciones de entrada / salida del sistema. Puede hacer esto usando la inyección de dependencia, dándonos una clase que puede solicitar nuevos enteros:

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

Luego puede crear pruebas para su función, usando un marco simulado (yo uso 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.");
}

Luego escribe tu función que pasa las pruebas. La función es mucho más limpia, ya que puede eliminar la duplicación de enteros preguntando / obteniendo y las llamadas al sistema reales están encapsuladas.

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

Esto puede parecer excesivo para su pequeño ejemplo, pero si está construyendo una aplicación más grande, el desarrollo de este tipo puede resultar bastante rápido.

Garrett Hall
fuente
5

Una forma común de probar un código similar sería extraer un método que incluya un Scanner y un PrintWriter, similar a esta respuesta de StackOverflow , y probar que:

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

Tenga en cuenta que no podrá leer su salida hasta el final, y tendrá que especificar toda su entrada por adelantado:

@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.")););
}

Por supuesto, en lugar de crear un método de sobrecarga, también puede mantener el "escáner" y la "salida" como campos mutables en su sistema bajo prueba. Me gusta mantener las clases lo más apátridas posible, pero eso no es una concesión muy grande si le importa a usted oa sus compañeros de trabajo / instructor.

También puede optar por poner su código de prueba en el mismo paquete Java que el código bajo prueba (incluso si está en una carpeta de origen diferente), lo que le permite relajar la visibilidad de la sobrecarga de dos parámetros para que sea privado del paquete.

Jeff Bowman
fuente
¿Quiso decir para probar, el método processUserInput () invoca el método (in, out) y no processUserInput (in, out)?
NadZ
@NadZ Por supuesto; Comencé con un ejemplo general y lo cambié de manera incompleta para que fuera específico para la pregunta.
Jeff Bowman
3

Me las arreglé para encontrar una forma más sencilla. Sin embargo, debe usar la biblioteca externa System.rules de @Stefan Birkner

Solo tomé el ejemplo que se proporciona allí, creo que no podría haber sido más simple:

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

Prueba

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

El problema, sin embargo, en mi caso, mi segunda entrada tiene espacios y esto hace que todo el flujo de entrada sea nulo.

Omar Elshal
fuente
Esto funciona muy bien ... No sé qué quieres decir con "flujo de entrada nulo". Lo bueno es que inputEntry = scanner.nextLine();cuando se usa con System.insiempre esperará al usuario (y aceptará una línea vacía como vacía String) ... mientras que cuando suministre líneas, systemInMock.provideLines()esto arrojará NoSuchElementExceptioncuando las líneas se agoten. Esto realmente hace que sea fácil no "distorsionar" demasiado el código de la aplicación para adaptarse a las pruebas.
Mike Rodent
No recuerdo exactamente cuál fue el problema, pero tienes razón, revisé mi código nuevamente y noté que lo arreglé de dos maneras: 1. Usando systemInMock: systemInMock.provideLines("1", "2"); 2. Usando System.setIn sin bibliotecas externas:String data2 = "1 2"; System.setIn(new ByteArrayInputStream(data2.getBytes()));
Omar Elshal
2

Puede comenzar extrayendo la lógica que recupera el número del teclado en su propio método. Luego, puede probar la lógica de validación sin preocuparse por el teclado. Para probar la llamada keyboard.nextInt (), es posible que desee considerar el uso de un objeto simulado.

Sarah Haskins
fuente
2
Tenga en cuenta que no puede simular los objetos del escáner (son definitivos).
hoipolloi
2

He solucionado el problema de leer desde stdin para simular una consola ...

Mi problema era que me gustaría intentar escribir en JUnit, probar la consola para crear un objeto determinado ...

El problema es como todo lo que dices: ¿Cómo puedo escribir en la prueba Stdin de JUnit?

Luego, en la universidad, aprendo sobre las redirecciones como dices System.setIn (InputStream), cambia el descriptor de archivo stdin y puedes escribir luego ...

Pero hay un problema más que solucionar ... el bloque de prueba JUnit esperando la lectura de su nuevo InputStream, por lo que necesita crear un hilo para leer desde InputStream y desde JUnit test Thread escribir en el nuevo Stdin ... Primero tiene que escriba en Stdin porque si escribe más tarde o crea el Thread para leer desde stdin, es probable que tenga condiciones de carrera ... puede escribir en InputStream antes de leer o puede leer desde InputStream antes de escribir ...

Este es mi código, mi habilidad en inglés es mala. Espero que todos puedan entender el problema y la solución para simular la escritura en stdin de la prueba 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();      
}
Javier Gutiérrez-Maturana Sánc
fuente
1

Me ha resultado útil crear una interfaz que defina métodos similares a java.io.Console y luego usarla para leer o escribir en System.out. La implementación real se delegará en System.console () mientras que su versión de JUnit puede ser un objeto simulado con entrada predefinida y respuestas esperadas.

Por ejemplo, construiría una MockConsole que contuviera la entrada enlatada del usuario. La implementación simulada sacaba una cadena de entrada de la lista cada vez que se llamaba a readLine. También recopilaría toda la salida escrita en una lista de respuestas. Al final de la prueba, si todo salió bien, entonces toda su entrada se habría leído y puede afirmar en la salida.

Massfords
fuente