Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can one JUnit test an interactive, text-based Java application?

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);
}
like image 406
ProfPlum Avatar asked Jan 22 '14 22:01

ProfPlum


2 Answers

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();
like image 99
Andrew Charneski Avatar answered Sep 28 '22 10:09

Andrew Charneski


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.

like image 22
kmera Avatar answered Sep 28 '22 09:09

kmera