Lecture 22: Tests and Debugging Practicum

Announcements

Assignment 11 posted, due next Wednesday. Gradescope autograder planned to go up sometime tomorrow.

Testing and debugging

Today we’re going to run through something a little different. I’ve gotten a lot of requests to go over testing and debugging in more detail, so today is going to be a demo of two things: how to write JUnit-style tests, and how to use the Eclipse debugger. Our sample application will be to build a class to support the game of Tic-Tac-Toe, with a particular (defined) interface. Here is the skeleton:

package tictactoe;

/**
 * An implementation of the TicTacToeGame interface.
 *
 * The board is a n x n set of spaces. Players, identified by "X" or "O", take
 * turns claiming spaces on the board, with "X" going first. The goal is to claim
 * n spaces in a line, horizontally, vertically, or diagonally. The game ends
 * when one player does so, or when no empty spaces remain.
 *
 * The spaces are numbered left-to-right and top-to-bottom, starting at 0.
 * For the traditional n=3 game the empty board looks like:
 *
 *      | |                              0|1|2
 *     -+-+-                             -+-+-
 *      | |     which is numbered as     3|4|5
 *     -+-+-                             -+-+-
 *      | |                              6|7|8
 *      
 */
public class TicTacToe implements TicTacToeGame {
    /**
     * Constructs a new instance, implementing the TicTacToeGame interface.
     *
     * @param n the length and width of the board; n >= 3
     */
    public TicTacToe(int n) {
    }

    /**
     * @return the value of n for this game
     */
    @Override
    public int getN() {
        return 0;
    }

    /**
     * Returns a multi-line string representation of the game.
     *
     * The string should display the board line-by-line, corresponding
     * to the space numbering. Each line should consist only of "X"s,
     * "O"s, or " "s (empty spaces), corresponding to spaces claimed by
     * the given player (or no player). Between each line is a newline;
     * there is no newline at the end of the last string.
     *
     * For example, one such String for a 4x4 board might be:
     *
     * "X  O\nXO  \nO X \nXO"
     *
     * which, when printed, would display:
     *
     * X  O
     * XO  
     * O X
     * XO
     *
     * @return a human-readable version of the current game state
     */
    @Override
    public String toString() {
        return null;
    }

    /**
     * Returns the identity of the player who has won (either "X" or "O"),
     * or the empty string if neither player is currently the winner.
     * @return the winning player "X" or "O" (or "" if no winner)
     */
    @Override
    public String getWinner() {
        return null;
    }

    /**
     * Returns the identity of the player whose turn it is to place their
     * piece ("X", "O", or "" if the game is over).
     *
     * Games end when one player has claimed n spaces in a row, or
     * when the board is full, and neither player has won.
     *@return the current player "X" or "O", or "" if the game is over
     */
    @Override
    public String getCurrentPlayer() {
        return null;
    }

    /**
     * Returns true if the chosen move is valid, and false otherwise.
     *
     * Moves are valid if the game is not over, and if the chosen space exists
     * in this game, and is empty.
     *
     * This method must not actually perform the move!
     *
     * @param space
     *            the space number
     * @return true iff the chosen move is valid
     */
    @Override
    public boolean isValidMove(int space) {
        return false;
    }

    /**
     * Claims the space for the current player.
     *
     * @param space the space number
     * @throws IllegalArgumentException if the move is invalid, or the game is over
     */
    @Override
    public void move(int space) throws IllegalArgumentException {
    }
}

and here’s a “driver” style class:

package tictactoe;

import java.util.Scanner;

public class TicTacToeRunner {
    public static void main(String[] args) {
        Scanner conIn = new Scanner(System.in);
        try {
            System.out.println("Welcome to n x n Tic-Tac-Toe.");
            System.out.println("Choose a value for n: ");
            final int n = conIn.nextInt();

            TicTacToeGame game = new TicTacToe(n);

            String currentPlayer = game.getCurrentPlayer();
            while (!currentPlayer.equals("")) {
                System.out.println("Current board:");
                System.out.println(game.toString());
                boolean hasMoved = false;
                while (!hasMoved) {
                    System.out.println("It is " + currentPlayer + "'s turn. Enter a space to claim: ");
                    final int space = conIn.nextInt();
                    if (!game.isValidMove(space)) {
                        System.out.println("Invalid space.");
                        continue;
                    }
                    game.move(space);
                    hasMoved = true;
                    currentPlayer = game.getCurrentPlayer();
                }
            }
            System.out.print("Game over...");
            final String winner = game.getWinner();
            if (winner.equals("")) {
                System.out.println("It was a draw.");
            }
            else {
                System.out.println(winner + " has won!");
            }
        } finally {
            conIn.close();
        }
    }
}

And finally, a trivial “test” class:

package tictactoe;

import static org.junit.Assert.*;

import org.junit.Test;
import org.junit.rules.Timeout;
import org.junit.Before;
import org.junit.Rule;


public class TicTacToeTest {
    @Rule
    public Timeout globalTimeout = Timeout.seconds(10); // 10 seconds

    @Test
    public void testTruth() {
        assertTrue(true);
    }
}

We’ll start by commenting out the timeout, so that we can run the debugger freely, then off we go.

Note that to test, we need something that we can either call and directly observe its result (e.g., a method that returns a testable value), or we need to be able to change the object, then observe the change through another method. So sometimes we have to write more (or partially write) than one method in order to support some tests.

  • n and getN
  • move (just X)
  • toString
  • isValidMove (just off board)
  • getCurrentPlayer (just counts of xs/os)
  • getCurrentPlayer (has someone won? entails private isWinner method, also implicated by getWinner)
  • isWinner
  • getWinner

Here’s where we left off at the end of class with TicTacToe:

package tictactoe;

/**
 * An implementation of the TicTacToeGame interface.
 *
 * The board is a n x n set of spaces. Players, identified by "X" or "O", take
 * turns claiming spaces on the board, with "X" going first. The goal is to claim
 * n spaces in a line, horizontally, vertically, or diagonally. The game ends
 * when one player does so, or when no empty spaces remain.
 *
 * The spaces are numbered left-to-right and top-to-bottom, starting at 0.
 * For the traditional n=3 game the empty board looks like:
 *
 *      | |                              0|1|2
 *     -+-+-                             -+-+-
 *      | |     which is numbered as     3|4|5
 *     -+-+-                             -+-+-
 *      | |                              6|7|8
 *      
 */
public class TicTacToe implements TicTacToeGame {
    final private int n;
    final private String X_PLAYER = "X";
    private String[] board;

    /**
     * Constructs a new instance, implementing the TicTacToeGame interface.
     *
     * @param n the length and width of the board; n >= 3
     */
    public TicTacToe(int n) {
        this.n = n;
        board = new String[n * n];
        for (int i = 0; i < board.length; i++) {
            board[i] = " ";
        }
    }

    /**
     * @return the value of n for this game
     */
    @Override
    public int getN() {
        return n;
    }

    /**
     * Returns a multi-line string representation of the game.
     *
     * The string should display the board line-by-line, corresponding
     * to the space numbering. Each line should consist only of "X"s,
     * "O"s, or " "s (empty spaces), corresponding to spaces claimed by
     * the given player (or no player). Between each line is a newline;
     * there is no newline at the end of the last string.
     *
     * For example, one such String for a 4x4 board might be:
     *
     * "X  O\nXO  \nO X \nXO"
     *
     * which, when printed, would display:
     *
     * X  O
     * XO  
     * O X
     * XO
     *
     * @return a human-readable version of the current game state
     */
    @Override
    public String toString() {
        String result = "";
        for (int i = 0; i < board.length; i++) {
            result = result + board[i];
            if (((i + 1) % n) == 0) {
                result = result + "\n";
            }
        }
        return result;
    }

    /**
     * Returns the identity of the player who has won (either "X" or "O"),
     * or the empty string if neither player is currently the winner.
     * @return the winning player "X" or "O" (or "" if no winner)
     */
    @Override
    public String getWinner() {
        if (isWinner(X_PLAYER)) {
            return X_PLAYER;
        }
        if (isWinner("O")) {
            return "O";
        }
        return "";
    }

    private boolean isWinner(String player) {
        // rows
        for (int row = 0; row < n; row ++) {
            boolean winner = true;
            for (int col = 0; col < n; col++) {
                final int cell = (row * n) + col;
                if (!board[cell].equals(player)) {
                    winner = false;
                }
            }
            if (winner == true) return true;
        }

        // cols
        for (int col = 0; col < n; col++) {
            boolean winner = true;
            for (int row = 0; row < n; row++) {
                final int cell = (row * n) + col;
                if (!board[cell].equals(player)) {
                    winner = false;
                }
            }
            if (winner == true) return true;
        }

        // diag
        for (int i = 0; i < n; i = i + (n+1)) {
            boolean winner = true;
            if (!board[i].equals(player)) {
                winner = false;
            }
            if (winner == true) return true;
        }

        for (int i = 0; i < n; i = i + (n-1)) {
            boolean winner = true;
            if (!board[i].equals(player)) {
                winner = false;
            }
            if (winner == true) return true;
        }

        return false;
}

    /**
     * Returns the identity of the player whose turn it is to place their
     * piece ("X", "O", or "" if the game is over).
     *
     * Games end when one player has claimed n spaces in a row, or
     * when the board is full, and neither player has won.
     *@return the current player "X" or "O", or "" if the game is over
     */
    @Override
    public String getCurrentPlayer() {
        if (!getWinner().equals("")) {
            return "";
        }

        // count xs
        int xCount = 0;
        int oCount = 0;
        for (String s : board) {
            if (s.equals(X_PLAYER)) xCount++;
            if (s.equals("O")) oCount++;
        }

        if (xCount + oCount == n * n) {
            return "";
        }

        if (xCount == oCount) {
            return X_PLAYER;
        }
        else {
            return "O";
        }       
    }

    /**
     * Returns true if the chosen move is valid, and false otherwise.
     *
     * Moves are valid if the game is not over, and if the chosen space exists
     * in this game, and is empty.
     *
     * This method must not actually perform the move!
     *
     * @param space
     *            the space number
     * @return true iff the chosen move is valid
     */
    @Override
    public boolean isValidMove(int space) {
        return false;
    }

    /**
     * Claims the space for the current player.
     *
     * @param space the space number
     * @throws IllegalArgumentException if the move is invalid, or the game is over
     */
    @Override
    public void move(int space) throws IllegalArgumentException {
        if (space < 0 || space > board.length - 1) {
            throw new IllegalArgumentException();
        }
        board[space] = getCurrentPlayer();
    }

    public static void main(String[] args) {
        TicTacToe t = new TicTacToe(3);
        t.move(4);
        t.move(7);
        t.move(5);
        t.move(6);
        t.move(3);
        System.out.println(t);
    }
}

and with the tests:

package tictactoe;

import static org.junit.Assert.*;

import org.junit.Test;
import org.junit.rules.Timeout;
import org.junit.Before;
import org.junit.Rule;


public class TicTacToeTest {
//  @Rule
//  public Timeout globalTimeout = Timeout.seconds(10); // 10 seconds

    @Test
    public void testTruth() {
        assertTrue(true);
    }

    @Test
    public void testGetN() {
        TicTacToe t = new TicTacToe(3);
        assertEquals(3, t.getN());
    }

    @Test
    public void testGetN4() {
        TicTacToe t = new TicTacToe(4);
        assertEquals(4, t.getN());
    }

    @Test
    public void testMoveSimple() {
        TicTacToe t = new TicTacToe(3);
        t.move(4);
        assertEquals("   \n X \n   \n", t.toString());
    }

    @Test(expected = IllegalArgumentException.class)
    public void testMoveOutOfBounds() throws Exception {
        TicTacToe t = new TicTacToe(3);
        t.move(10);
    }

    @Test
    public void testMoveTwice() {
        TicTacToe t = new TicTacToe(3);
        t.move(4);
        t.move(3);
        assertEquals("   \nOX \n   \n", t.toString());
    }

}