Capturing Console Output With a Deque

Redirecting stdout to a new PrintStream is an easy way to test simple console output in Java. But as my Tic-Tac-Toe game has grown more complex, the tests I wrote using this pattern have started to stink. There’s a lot of duplicated code (create the stream, redirect, tear down with each test), each test uses a hand-constructed string filled with finicky newline characters, and tests are prone to break when unrelated view components change the way they print to the screen. Inspired by my input queue, I created an OutputRecorder class that extends PrintStream and captures output string by string for later playback:

OutputRecorder.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public class OutputRecorder extends PrintStream {
    private Deque<String> outputStack;

      public OutputRecorder(OutputStream outputStream, boolean b,
      String s) throws UnsupportedEncodingException {
          super(outputStream, b, s);
          outputStack = new LinkedList<String>();
        }

        private void catchOutput(String output) {
            outputStack.addFirst(output);
        }

        public String popLastOutput() {
            return outputStack.removeFirst();
        }

        public String popFirstOutput() {
            return outputStack.removeLast();
        }


        public String peekLastOutput() {
            return outputStack.peekFirst();
        }

        public String peekFirstOutput() {
            return outputStack.peekLast();
        }

        public void discardLastNStrings(int n) {
            for (int i=0; i < n; i++) {
                popLastOutput();
            }
       }

       public void discardFirstNStrings(int n) {
            for (int i=0; i < n; i++) {
                popFirstOutput();
            }
       }

       public void replayAllForwards() {
            String output = popFirstOutput();
            int i = 0;
            while (output != null) {
                System.out.println("Element" + i + ":");
                System.out.println(output);
                i += 1;
                output = popFirstOutput();
            }
       }

       public void replayAllBackwards() {
            String output = popLastOutput();
            int i = outputStack.size();
            while (output != null) {
                System.out.println("Element" + i + ":");
                System.out.println(output);
                i -= 1;
                output = popLastOutput();
            }
       }

       @Override
       public void println(String output) {
            catchOutput(output);
       }

       @Override
       public void print(String output) {
            catchOutput(output);
       }

}

Since the recorder stores strings in a deque, it’s easy to replay output in forward or reverse order. Now a test like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

@Test
public void controllerShouldPassErrorMessageToViewOnInvalidMove() {
    view.pushInput("middle center");
    view.pushInput("middle center");

    exit.expectSystemExitWithStatus(2);
    controller.newGame();

    System.setOut(outputStream);

    controller.playRound();
    String expected = yourMove + "\n" + xInCenter.toString() + "\n";

    assertEquals(expected,
    output.toString());

    System.setOut(stdout);
    view.clearInput();
}

Become a little friendlier…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
public void controllerShouldPassErrorMessageToViewOnInvalidMove() throws GameOverException {
    view.enqueueInput("middle center");
    view.enqueueInput("middle center");

    controller.newGame();

    System.setOut(outputRecorder);

    try {
        controller.playRound();
    } catch (NoSuchElementException e) {

          outputRecorder.discardFirstNStrings(4);
          String output = outputRecorder.popFirstOutput();

          assertEquals("Square is already full.", output);
    }
    System.setOut(stdout);
    view.clearInput();
}

The utility might not be immediately obvious, but capturing output string by string has already put an end to tracking down small differences between expected and actual output that come from an extra space or misplaced newline.

Comments