Test de JUnit avec entrée simulée de l'utilisateur

j'essaie de créer des tests JUnit pour une méthode qui nécessite l'entrée de l'utilisateur. La méthode à l'essai ressemble quelque peu à la méthode suivante:

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

y a-t-il un moyen de passer automatiquement le programme avec un int au lieu de moi ou de quelqu'un d'autre faisant cela manuellement dans la méthode de test de JUnit? Comme Simuler l'entrée de l'utilisateur?

Merci d'avance.

57
demandé sur Wimpey 2011-06-20 22:28:20

7 réponses

vous pouvez remplacer système.avec votre propre flux en appelant le système.setIn(InputStream) . Le flux d'entrée peut être un tableau d'octets:

ByteArrayInputStream in = new ByteArrayInputStream("My string".getBytes());
System.setIn(in);

// do your thing

// optionally, reset System.in to its original
System.setIn(System.in)

une approche différente peut rendre cette méthode plus testable en passant dans et Hors comme paramètres:

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;
}
79
répondu KrzyH 2014-05-14 12:36:02

pour tester votre code, vous devez créer un wrapper pour les fonctions d'entrée/sortie du système. Vous pouvez faire cela en utilisant l'injection de dépendances, nous donnant une classe qui peut demander de nouveaux entiers:

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

alors vous pouvez créer des tests pour votre fonction, en utilisant un cadre simulé (J'utilise 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.");
}

alors écrivez votre fonction qui passe les tests. La fonction est beaucoup plus propre puisque vous pouvez enlever l'entier de demande/réception la duplication et les appels système réels sont encapsulés.

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

cela peut sembler exagéré pour votre petit exemple, mais si vous construisez une application plus grande se développer comme ceci peut rapporter assez rapidement.

15
répondu Garrett Hall 2011-06-21 10:13:42

une façon courante de tester un code similaire serait d'extraire une méthode qui prend dans un Scanner et un imprimeur, semblable à cette réponse de débordement de piles , et tester 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;
}

notez que vous ne pourrez pas lire votre sortie avant la fin, et vous devrez spécifier toutes vos entrées à l'avance:

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

bien sûr, plutôt que de créer une méthode de surcharge, vous pourriez également garder le "scanner" et "sortie" comme champs mutables dans votre système sous test. J'ai tendance à aimer garder les cours aussi apatrides que possible, mais ce n'est pas une très grande concession si cela compte pour vous ou vos collègues/instructeur.

vous pouvez également choisir de mettre votre code test dans le même paquet Java que le code à l'essai (même s'il est dans un autre dossier source), ce qui vous permet de détendre la visibilité des deux paramètres surcharge à être paquet-privé.

5
répondu Jeff Bowman 2017-05-23 12:18:16

j'ai réussi à trouver un moyen plus simple. Cependant, vous devez utiliser la bibliothèque externe système.règles par @ Stefan Birkner

j'ai juste pris l'exemple fourni là, je pense qu'il ne pouvait pas être plus 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;
  }
}
Test

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

le problème cependant dans mon cas mon second input ont des espaces et cela rend le flux d'entrée entier nul !

2
répondu Omar Elshal 2016-11-15 20:16:06

vous pourriez commencer par extraire la logique qui récupère le nombre du clavier dans sa propre méthode. Ensuite, vous pouvez tester la logique de validation, sans vous soucier du clavier. Afin de tester le clavier.appel nextInt () vous pourriez vouloir considérer l'utilisation d'un objet simulé.

1
répondu Sarah Haskins 2011-06-20 18:43:03

j'ai trouvé utile de créer une interface qui définit des méthodes similaires à java.io.Console et ensuite l'utiliser pour lire ou écrire sur le système.hors. La mise en œuvre réelle sera déléguée au système.console () alors que votre version de JUnit peut être un objet simulé avec des entrées en boîte et des réponses attendues.

par exemple, vous construiriez un MockConsole qui contenait les entrées en boîte de l'utilisateur. L'implémentation simulée ferait apparaître une chaîne de caractères d'entrée hors de la liste chaque temps readLine a été appelé. Il rassemblerait également tous les résultats écrits à une liste de réponses. A la fin du test, si tout s'est bien passé, alors toute votre entrée aurait été lue et vous pouvez l'affirmer sur la sortie.

1
répondu massfords 2011-06-20 19:29:36

j'ai corrigé le problème de lecture de stdin pour simuler une console...

mes problèmes étaient que j'aimerais essayer d'écrire dans JUnit tester la console pour créer un certain objet...

le problème est comme tout ce que vous dites : Comment puis-je écrire dans le test STDIN from JUnit?

puis à l'Université j'apprends sur les redirections comme vous dites système.setIn (InputStream) changer le STDIN filedescriptor et vous pouvez écrire alors...

mais il reste un problème à régler... le bloc de test JUnit en attente lire à partir de votre nouvelle entrée, donc vous avez besoin de créer un thread pour lire à partir de la entrée et de JUnit test Thread écrire dans le nouveau Stdin... Tout d'abord, vous devez écrire dans le Stdin parce que si vous écrivez plus tard de créer le fil à lire à partir de stdin, vous aurez probablement conditions de course... vous pouvez écrire dans le flux D'entrée avant de lire ou vous pouvez lire depuis le flux d'entrée avant d'écrire...

C'est mon code, mon anglais est mauvais j'espère que tout ce que vous pouvez comprendre le problème et la solution pour simuler écrire dans stdin de test de 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();      
}
1
répondu Javier Gutiérrez-Maturana Sánc 2017-03-27 16:09:24