web-dev-qa-db-fra.com

Test JUnit avec entrée utilisateur simulée

J'essaie de créer des tests JUnit pour une méthode nécessitant une intervention de l'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 possible de transmettre automatiquement au programme un int à la place de moi ou de le faire manuellement, dans la méthode de test JUnit? Vous aimez simuler la saisie de l'utilisateur?

Merci d'avance.

70
Wimpey

Vous pouvez remplacer System.in par votre propre flux en appelant System.setIn (InputStream in) . 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 donnant les paramètres IN et OUT:

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

Pour tester votre code, vous devez créer un wrapper pour les fonctions d’entrée/sortie du système. Vous pouvez le faire en utilisant l'injection de dépendance, ce qui nous donne une classe pouvant 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 fictif (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.");
}

Ensuite, écrivez votre fonction qui passe les tests. La fonction est beaucoup plus propre puisque vous pouvez supprimer la duplication d’entier demandant/obtenu et que 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 excessif pour votre petit exemple, mais si vous construisez une application plus grande, le développement de cette manière peut rapporter assez rapidement.

16
Garrett Hall

Un moyen courant de tester un code similaire consiste à extraire une méthode prenant un scanner et un PrintWriter, similaire à cette réponse StackOverflow , et de vérifier 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 toute votre entrée au début:

@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 entendu, au lieu de créer une méthode de surcharge, vous pouvez également conserver les champs "scanner" et "sortie" en tant que champs modifiables dans votre système testé. J'ai tendance à vouloir que les cours soient aussi apatrides que possible, mais ce n'est pas une très grosse concession si cela compte pour vous ou vos collègues/instructeurs.

Vous pouvez également choisir de placer votre code de test dans le même Java que le code sous test (même s'il se trouve dans un dossier source différent)), ce qui vous permet de relâcher la visibilité des deux paramètres. surcharge pour être paquet-privé.

5
Jeff Bowman

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

Je viens de prendre l'exemple fourni ici, je pense que cela n'aurait pas pu ê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, ma deuxième entrée a des espaces et cela rend le flux d’entrée entier nul!

2
Omar Elshal

Vous pouvez commencer par extraire la logique qui extrait le numéro du clavier dans sa propre méthode. Ensuite, vous pouvez tester la logique de validation sans vous soucier du clavier. Afin de tester l'appel keyboard.nextInt (), vous pouvez envisager d'utiliser un objet fictif.

2
Sarah Haskins

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

Mon problème était que je voudrais 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?

Ensuite, au collège, je découvre des redirections comme vous dites: System.setIn (InputStream) modifie le FileScriptor stdin et vous pouvez écrire ensuite ...

Mais il reste encore un problème à résoudre: le bloc de test JUnit en attente de lecture de votre nouveau InputStream. Vous devez donc créer un thread à lire à partir de InputStream et à partir du test de JUnit écrire dans le nouveau Stdin ... écrivez dans le Stdin parce que si vous écrivez plus tard de créer le fil à lire à partir de stdin, vous aurez probablement des conditions de concurrence ... vous pouvez écrire dans InputStream avant de lire ou vous pouvez lire à partir de InputStream avant d'écrire ...

Ceci est mon code, mes compétences en anglais sont mauvaises. J'espère que tout le monde comprendra le problème et la solution pour simuler l'écriture en stdin à partir du test 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();      
}

J'ai trouvé utile de créer une interface définissant des méthodes similaires à Java.io.Console, puis de l'utiliser pour la lecture ou l'écriture sur System.out. L'implémentation réelle déléguera à System.console () tandis que votre version de JUnit peut être un objet fictif avec une entrée prédéfinie et les réponses attendues.

Par exemple, vous construiriez une MockConsole qui contiendrait l'entrée prédéfinie de l'utilisateur. L'implémentation factice ferait apparaître une chaîne d'entrée de la liste à chaque appel de readLine. Cela permettrait également de rassembler toutes les sorties écrites dans une liste de réponses. À la fin du test, si tout se passait bien, toutes vos contributions auraient été lues et vous pourrez affirmer leurs résultats.

1
massfords