Ideally, I would like to write JUnit test code that interactively tests student text-based I/O applications. Using System.setIn()
/.setOut()
leads to problems because the underlying streams are blocking. Birkner's System Rules (http://www.stefan-birkner.de/system-rules/index.html) was recommended in an earlier post (Testing console based applications/programs - Java), but it appears to require all standard input to be provided before the unit test target is run and is thus not interactive.
To provide a concrete test target example, consider this guessing game code:
public static void guessingGame() {
Scanner scanner = new Scanner(System.in);
Random random = new Random();
int secret = random.nextInt(100) + 1;
System.out.println("I'm thinking of a number from 1 to 100.");
int guess = 0;
while (guess != secret) {
System.out.print("Your guess? ");
guess = scanner.nextInt();
final String[] responses = {"Higher.", "Correct!", "Lower."};
System.out.println(responses[1 + new Integer(guess).compareTo(secret)]);
}
}
Now imagine a JUnit test that would be providing guesses, reading responses, and playing the game to completion. How might one accomplish this in a JUnit testing framework?
ANSWER:
Using the approach recommended by Andrew Charneski below, adding output flushing (including adding System.out.flush();
after each print statement above), non-random play, and restoration of System.in/out, this code seems to perform the test I was imagining:
@Test
public void guessingGameTest() {
final InputStream consoleInput = System.in;
final PrintStream consoleOutput = System.out;
try {
final PipedOutputStream testInput = new PipedOutputStream();
final PipedOutputStream out = new PipedOutputStream();
final PipedInputStream testOutput = new PipedInputStream(out);
System.setIn(new PipedInputStream(testInput));
System.setOut(new PrintStream(out));
new Thread(new Runnable() {
@Override
public void run() {
try {
PrintStream testPrint = new PrintStream(testInput);
BufferedReader testReader = new BufferedReader(
new InputStreamReader(testOutput));
assertEquals("I'm thinking of a number from 1 to 100.", testReader.readLine());
int low = 1, high = 100;
while (true) {
if (low > high)
fail(String.format("guessingGame: Feedback indicates a secret number > %d and < %d.", low, high));
int mid = (low + high) / 2;
testPrint.println(mid);
testPrint.flush();
System.err.println(mid);
String feedback = testReader.readLine();
if (feedback.equals("Your guess? Higher."))
low = mid + 1;
else if (feedback.equals("Your guess? Lower."))
high = mid - 1;
else if (feedback.equals("Your guess? Correct!"))
break;
else
fail("Unrecognized feedback: " + feedback);
}
} catch (IOException e) {
e.printStackTrace(consoleOutput);
}
}
}).start();
Sample.guessingGame();
}
catch (IOException e) {
e.printStackTrace();
fail(e.getMessage());
}
System.setIn(consoleInput);
System.setOut(consoleOutput);
}
Use PipedInput/OutputStream, e.g.
final PrintStream consoleOutput = System.out;
final PipedOutputStream testInput = new PipedOutputStream();
final PipedOutputStream out = new PipedOutputStream();
final PipedInputStream testOutput = new PipedInputStream(out);
System.setIn(new PipedInputStream(testInput));
System.setOut(new PrintStream(out));
new Thread(new Runnable() {
@Override
public void run() {
try {
PrintStream testPrint = new PrintStream(testInput);
BufferedReader testReader = new BufferedReader(
new InputStreamReader(testOutput));
while (true) {
testPrint.println((int) (Math.random() * 100));
consoleOutput.println(testReader.readLine());
}
} catch (IOException e) {
e.printStackTrace(consoleOutput);
}
}
}).start();
guessingGame();
The best approach would be to separate the input and game logic.
Create an interface for the input part (with a method like getNextGuess
) and a concrete implementation where you put your scanner. That way you could also extend/exchange it later on. And in your unit tests you can then mock that class to provide the input you need to test.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With