93 votes

Test JUnit avec entrée utilisateur simulée

J'essaie de créer des tests JUnit pour une méthode qui nécessite une entrée utilisateur. La méthode testée ressemble un 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;
}

Existe-t-il un moyen de passer automatiquement un int au programme au lieu que moi ou quelqu'un d'autre le fasse manuellement dans la méthode de test JUnit ? Comme simuler l'entrée de l'utilisateur ?

Merci d'avance.

7 votes

Que testez-vous, exactement ? Un scanner ? Une méthode de test doit typiquement affirmer quelque chose pour être utile.

2 votes

Vous ne devriez pas avoir à tester la classe Scanner de Java. Vous pourriez définir manuellement votre entrée et simplement tester votre propre logique. int input = -1 ou 5 ou 11 couvrira votre logique

3 votes

Six ans plus tard... et c'est toujours une bonne question. Notamment parce que lorsque vous commencez à développer une application, vous n'avez généralement pas envie d'avoir toutes les cloches et les sifflets de JavaFX, mais plutôt de commencer à utiliser l'humble ligne de commande pour un peu d'interaction très basique. Il est dommage que JUnit ne rende pas cela un peu plus facile. Pour moi, la réponse d'Omar Elshal est très bien, avec un minimum de codage d'applications "artificielles" ou "déformées"...

117voto

KrzyH Points 1915

Vous pouvez remplacer System.in avec votre propre flux en appelant System.setIn(InputStream in) . InputStream peut être un tableau d'octets :

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

Une approche différente peut rendre cette méthode plus testable en passant IN et OUT 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;
}

7 votes

@KrzyH comment pourrais-je faire cela avec des entrées multiples ? Disons que dans //do your thing, j'ai trois invites à saisir pour l'utilisateur. Comment dois-je m'y prendre ?

1 votes

@Stupid.Fat.Cat J'ai ajouté une deuxième approche du problème, plus élégante et plus facile à mettre en œuvre.

1 votes

@KrzyH Pour la deuxième approche, vous devrez passer le flux d'entrée et le flux de sortie en paramètre dans la définition de la méthode, ce qui n'est pas ce que je recherche. Connaissez-vous une meilleure méthode ?

20voto

Garrett Hall Points 11902

Pour tester votre code, vous devez créer une enveloppe pour les fonctions d'entrée/sortie du système. Vous pouvez le faire en utilisant l'injection de dépendances, en 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();
    }
}

Ensuite, vous pouvez créer des tests pour votre fonction, en utilisant un framework mock (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.");
}

Puis écrivez votre fonction qui passe les tests. La fonction est beaucoup plus propre, car vous pouvez supprimer la duplication de la demande et de l'obtention des entiers 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 importante, un tel développement peut s'avérer rapidement payant.

6voto

Jeff Bowman Points 9712

Une façon courante de tester un code similaire serait d'extraire une méthode qui prend en compte un Scanner et un PrintWriter, comme dans le cas suivant cette réponse de StackOverflow et de le tester :

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 vos résultats avant la fin, et que vous devrez spécifier toutes vos entrées au préalable :

@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 aussi conserver le "scanner" et la "sortie" comme champs mutables dans votre système à tester. J'ai tendance à préférer garder les classes aussi apatrides que possible, mais ce n'est pas une concession très importante si cela compte pour vous ou vos collègues/instructeurs.

Vous pouvez également choisir de placer votre code de test dans le même paquetage Java que le code testé (même s'il se trouve dans un dossier source différent), ce qui vous permet d'assouplir la visibilité de la surcharge à deux paramètres pour qu'elle soit privée par paquetage.

4voto

Omar Elshal Points 43

J'ai réussi à trouver un moyen plus simple. Cependant, vous devez utiliser une bibliothèque externe System.rules par @Stefan Birkner

J'ai juste pris l'exemple fourni là, je pense que ça 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 est que dans mon cas, ma deuxième entrée contient des espaces et cela rend le flux d'entrée entier nul !

0 votes

Cela fonctionne très bien... Je ne sais pas ce que vous voulez dire par "input stream null". Ce qui est bien, c'est que inputEntry = scanner.nextLine(); lorsqu'il est utilisé avec System.in attendra toujours l'utilisateur (et acceptera une ligne vide comme une ligne vide String )... alors que lorsque vous fournissez des lignes en utilisant systemInMock.provideLines() cela jettera NoSuchElementException quand les lignes seront épuisées. Cela permet de ne pas trop "déformer" le code de l'application pour répondre aux besoins des tests.

0 votes

Je ne me rappelle pas exactement quel était le problème, mais vous avez raison, j'ai vérifié à nouveau mon code et j'ai remarqué que je l'ai corrigé de deux façons : 1. En utilisant systemInMock : systemInMock.provideLines("1", "2"); 2. Utiliser System.setIn sans bibliothèques externes : String data2 = "1 2"; System.setIn(new ByteArrayInputStream(data2.getBytes()));

3voto

Sarah Haskins Points 645

Vous pourriez commencer par extraire la logique qui récupère le numéro du clavier dans sa propre méthode. Vous pourrez alors tester la logique de validation sans vous soucier du clavier. Pour tester l'appel keyboard.nextInt(), vous pouvez envisager d'utiliser un objet fantaisie.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X