J'ai un programme en ligne de commande Java. Je voudrais créer un cas de test JUnit pour pouvoir simuler System.in
. Parce que lorsque mon programme s'exécutera, il entrera dans la boucle while et attendra les commentaires des utilisateurs. Comment puis-je simuler cela dans JUnit?
Merci
Il est techniquement possible de changer System.in
, mais en général, il serait plus robuste de ne pas l'appeler directement dans votre code, mais d'ajouter une couche d'indirection afin que la source d'entrée soit contrôlée à partir d'un point de votre application. La manière dont vous le faites est un détail d’implémentation - les suggestions d’injection de dépendance conviennent, mais vous n’avez pas nécessairement besoin d’introduire des frameworks tiers; vous pouvez par exemple contourner un contexte d'E/S à partir du code d'appel.
Comment changer 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);
}
Il y a plusieurs façons d'aborder cela. Le moyen le plus complet consiste à transmettre un InputStream lors de l'exécution de la classe à tester, qui est un faux InputStream qui transmet des données simulées à votre classe. Vous pouvez consulter un framework d'injection de dépendance (tel que Google Guice) si vous devez le faire souvent dans votre code, mais la méthode la plus simple est la suivante:
public class MyClass {
private InputStream systemIn;
public MyClass() {
this(System.in);
}
public MyClass(InputStream in) {
systemIn = in;
}
}
Sous test, vous appelez le constructeur qui prend le flux d'entrée. Vous mettez même en nuage ce paquet constructeur et placez le test dans le même paquet, de sorte qu'un autre code n'envisage généralement pas de l'utiliser.
Basé sur la réponse de @ McDowell et une autre réponse qui montre comment tester System.out , j'aimerais partager ma solution pour donner une entrée à un programme et en tester le résultat.
En guise de référence, j'utilise JUnit 4.12.
Disons que nous avons ce programme qui réplique simplement les entrées vers les sorties:
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();
}
}
Pour le tester, nous pouvons utiliser la classe suivante:
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());
}
}
Je n'expliquerai pas grand chose, car je crois que le code est lisible et j'ai cité mes sources.
Lorsque JUnit exécute testCase1()
, il appelle les méthodes d'assistance dans l'ordre dans lequel elles apparaissent:
setUpOutput()
, à cause de l'annotation @Before
provideInput(String data)
, appelé depuis testCase1()
getOutput()
, appelé depuis testCase1()
restoreSystemInputOutput()
, à cause de l'annotation @After
Je n'ai pas testé System.err
parce que je n'en avais pas besoin, mais il devrait être facile à mettre en œuvre, comme dans le test System.out
.
Essayez de refactoriser votre code pour utiliser injection de dépendance . Au lieu d’avoir votre méthode a qui utilise System.in
directement, demandez à la méthode d’accepter une variable InputStream
comme argument. Ensuite, dans votre test Junit, vous pourrez passer une implémentation de test InputStream
à la place de System.in
.
Vous pouvez écrire un test clair pour l'interface de ligne de commande à l'aide de la règle TextFromStandardInputStream
de la System Rules library.
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());
}
}
Divulgation complète: je suis l'auteur de cette bibliothèque.
Le problème avec BufferedReader.readLine()
est qu’il s’agit d’une méthode de blocage qui attend les entrées de l’utilisateur. Il me semble que vous ne voulez pas particulièrement simuler cela (c’est-à-dire que vous voulez que les tests soient rapides). Mais dans un contexte de test, il renvoie continuellement null
à grande vitesse pendant les tests, ce qui est fastidieux.
Pour un puriste, vous pouvez rendre la getInputLine
ci-dessous package-private et vous en moquer: easy-peezy.
String getInputLine() throws Exception {
return br.readLine();
}
... vous devez vous assurer que vous avez le moyen d'arrêter (généralement) une boucle d'interaction de l'utilisateur avec l'application. Vous devrez également faire face au fait que vos "lignes de saisie" seront toujours les mêmes jusqu'à ce que vous changiez d'une manière ou d'une autre la doReturn
de votre maquette, ce qui est peu typique d'une saisie utilisateur.
Pour un non-puriste qui souhaite se simplifier la vie (et produire des tests lisibles), vous pouvez mettre tous ces éléments ci-dessous dans le code de votre application:
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;
}
... dans votre test, vous pourriez alors aller comme ceci:
consoleHandler.setInputLines( Arrays.asList( new String[]{ "first input line", "second input line" }));
avant de déclencher la méthode dans cette classe "ConsoleHandler" qui nécessite des lignes d'entrée.
peut-être comme ça (non testé):
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 );
plus de pièces:
//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);