Wednesday, December 21, 2011

Testing Java Console Applications

Currently I'm reading the great book "Growing Object-Oriented Software, Guided by Tests" by Steve Freeman and Nat Pryce. They encourage us to drive software development in the large with an outer loop of end-to-end acceptance tests and in the small with an inner loop of unit tests. While implementing just for fun the Nine Men's Morris board game with a simple console user interface, I tried to get a feeling for "test guided growing of software" as it is described in the book.

During that exercise I needed to find a solution to control the input towards and the output from the console. The following example source code shows one possible solution.

I started with an acceptance test. I choose to implement it with JUnit, but Fitnesse tool would have been an alternative.

public class NineMensMorrisAcceptanceTests {

  private ApplicationRunner application = new ApplicationRunner();
  
  @Test
  public void applicationAsksForUserMoveAndThenMakesOwnMove()
  {
    application.startGame();
    application.hasDisplayed("Nine Men's Morris");
    application.hasDisplayed("Please enter spot to place piece:");
    application.userEnters("1\r\n");
    application.hasDisplayed("Computer places piece on spot: 2");
  }
}
The ApplicationRunner class starts the console application in a new thread and acquires control over the input and output streams. Luckily Java has such a well designed IO system, which allows easy test set up. The game application writes to the console via System.out and reads from the console via System.in:
public class ApplicationRunner {

  private PipedOutputStream pipedOutputStream;
  private PipedInputStream pipedInputStream;
  private ByteArrayOutputStream outputStream;

  public ApplicationRunner(){
    pipedOutputStream = new PipedOutputStream();
    pipedInputStream = new PipedInputStream(pipedOutputStream);
    System.setIn(pipedInputStream);

    outputStream = new ByteArrayOutputStream();
    System.setOut(new PrintStream(outputStream));
  }
 
  public void startGame() {
    Thread thread = new Thread("Test Application"){
      @Override public void run(){Console.main(null);}
    };
    thread.setDaemon(true);
    thread.start();
  }

  public void hasDisplayed(String text) {
    boolean displayed = false; int tries = 20;
    while(tries>0 && !displayed){
      Thread.sleep(100);
      displayed = outputStream.toString().contains(text) ? true : false;
      tries--;
    }
    if (!displayed){
      throw new AssertionError("Missing text in output: " + text);
    }
  }

  public void userEnters(String userInput) {
    pipedOutputStream.write(userInput.getBytes());
  }
}
The Console.main() method set ups and starts the console application:
public static void main(String[] args) {
  ConsoleGameUI consoleGameUI = new ConsoleGameUI();
  GameController controller = new GameController(
    consoleGameUI, 
    new Engine(),
    new MoveGenerator());
  consoleGameUI.init(new InputParser(),controller);
  controller.start();
}
When we develop the ConsoleGameUI class, we will write some unit tests. There we can use also the hijacked streams to control inputs and outputs. Because this time the test runs synchronously we can use a ByteArrayInputStream instead of PipedInputStream to supply the user input to the system under test:
public class ConsoleGameUITests {
 
  // Class under test
  ConsoleGameUI consoleGameUI;

  private String userInput;
  private ByteArrayInputStream inputStream;
  private ByteArrayOutputStream outputStream;
  ...
 
  @Before public void setUp() {
    userInput = "some input from user";
    inputStream = new ByteArrayInputStream(userInput.getBytes());
    outputStream = new ByteArrayOutputStream();
    System.setIn(inputStream);
    System.setOut(new PrintStream(outputStream));
    ... 
    consoleGameUI = new ConsoleGameUI();
    consoleGameUI.init(inputParserMock, gameControllerMock);
  }
 
  @Test public void shouldPromptTheUserToEnterSpotToPlaceAPiece(){
    consoleGameUI.askUserForMove(Turn.PLACE_WHITE);
    assertTrue(outputStream.toString().contains("Please enter spot to place piece:"));
  }

  @Test public void shouldPromptTheUserToEnterSpotsToSlidePiece(){
    consoleGameUI.askUserForMove(Turn.SLIDE_WHITE);
    assertTrue(outputStream.toString().contains("Please enter spots to slide piece:"));
  }

  @Test public void shouldReadInputAndCallParser()
  {
    context.checking(new Expectations() {{
      oneOf(inputParserMock).Parse(userInput);
      ...  
    }});
    consoleGameUI.askUserForMove(Turn.PLACE_WHITE);
    context.assertIsSatisfied();
  }
  ...
}
In the ConsoleGameUI class we use System.out and System.in:
public class ConsoleGameUI implements GameUI {

  private final Scanner scanner;
  private IInputParser parser;
  private IGameController gameController;
 
  public ConsoleGameUI(){
    this.scanner = new Scanner(System.in);
  }
 
  public void init(IInputParser parser, IGameController gameController){...}
 
  @Override
  public MoveRequest askUserForMove(Turn turn) {
    switch(turn){
    case PLACE_WHITE:
      System.out.print("Please enter spot to place piece:"); 
      break;
    case SLIDE_WHITE:
      System.out.print("Please enter spots to slide piece:");
      break;
    ...  
    String line = scanner.nextLine();
    MoveRequest request = parser.Parse(line);
    return request;
  }
  ...
}

2 comments:

vecin said...
This comment has been removed by the author.
vecin said...

its curious, I read the same book and I've decided to implement a console game as well, Checkers.
Really helpful your stuff I was struggling quite a bit in how to test console ui. Ill follow quite a bit of you stuff.

Thanks a lot Sven,
David