6

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);
}
Community
  • 1
  • 1
ProfPlum
  • 129
  • 1
  • 8
  • Do you mean something like [Sikuli](http://www.sikuli.org/)? – Edwin Dalorzo Jan 22 '14 at 22:38
  • Sikuli appears to be oriented to graphical user interfaces, whereas I'm focusing on text-based I/O. Thanks for the link, though. I'll tuck that away for another day. – ProfPlum Jan 23 '14 at 15:54

4 Answers4

3

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.

kmera
  • 1,725
  • 10
  • 22
  • 1
    I agree that this would be the best approach for most contexts. Specifying Model-View-Controller ([link](http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller)) helps, but presents problems for introductory CS students that are just a few steps beyond "Hello, world!" and not yet comfortable with O-O. This is the zone I'm seeking to develop JUnit tests for. Ideally, they can not separate into MVC for now, and I can compensate with more sophisticated testing. – ProfPlum Jan 23 '14 at 12:12
3

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();
  • This approach works for for my purposes. There is one improvement I would suggest. On my machines, the above approach leads to one second lags between turns on my system. What I've found is that calling flush() after printing on both ends eliminates these one second lags. I'll add non-random test code using this approach momentarily. – ProfPlum Jan 23 '14 at 15:42
1

Maybe you should ask yourself which properties of the code you want to ensure. Eg: you might want to make sure that the correct response is given depending on the input. Then you should refactor your code and extract a function like

String getResponse(int secret, int guess) 
{
    ...
}

Then you can test

AssertEquals("Higher.",getResponse(50,51));
AssertEquals("Correct!",getResponse(50,50));
AssertEquals("Lower.",getResponse(50,49));

It doesn't make much sense to test the complete flow including random numbers. You might make a testloop 0..100 but it's better to test lower/upper end and something in between. And you don't need interactive input in a unit test. Doesn't make sense. It can only be lower, higher or equal.

  • Whereas I would separate the assignment specification into Model-View-Controller abstractions for more advanced students, the target of testing here is for students that are just beyond "Hello, world!", learning simple I/O, control flow, etc., and not deeply O-O yet. Once they've master O-O basics, I agree that such separation of concerns is wise. – ProfPlum Jan 23 '14 at 12:17
0

I would wrap System.in and System.out in an object which you can then inject. This way, you can inject a mock in its place in your junit tests. Will also be good for swapping what you might want to use for input and output in the future! :)

crebma
  • 71
  • 2