InformIT

Crafting Java with Test-Driven Development, Part 6: Refactoring Tests

Date: Mar 10, 2006

Return to the article

With the support mechanisms in place and well-tested, Jeff Langr's poker game seems to have reached the point that developers like best: banging out the code. But even at this point, he's careful to focus on test-driving classes into existence.

Adding a Game Class, Test-First

Our progress in building the poker game has been a bit sluggish. Learning something new, particularly something as dramatic as test-driven development (TDD), doesn’t come for free. But now that we’ve gotten past learning some of the basics, we should start to see some increased productivity. In this installment, we’ll try to start banging out some code.

We currently have a couple of core classes, Deck and Card. On their own, they’re not very useful, but they are key to building the Texas Hold ’Em application.

Let’s move on and build a Game class—test-first, of course (see Listing 1). We can play Texas Hold ’Em with up to 10 players. We’ll drive out some simple support for creating a game and for adding a single player to the game.

Listing 1 Starter tests for the Game class.

package domain;

import java.util.*;
import junit.framework.*;

public class GameTest extends TestCase {
  private Game game;

  protected void setUp() {
   game = new Game();
  }

  public void testCreate() {
   assertEquals(0, game.players().size());
  }

  public void testAddSinglePlayer() {
   final String player = "Jeff";
   game.add(player);
   List<String> players = game.players();
   assertEquals(1, players.size());
   assertEquals(player, players.get(0));
  }
}

The production code we end up with that meets these test specifications is pretty straightforward (see Listing 2).

Listing 2 Initial Game implementation.

package domain;

import java.util.*;

public class Game {
  private List<String> players = new ArrayList<String>();

  public void add(String player) {
   players.add(player);
  }

  public List<String> players() {
   return players;
  }
}

The derivation of this code isn’t nearly as straightforward. We don’t type these two classes all at once. Test-driving them into existence means a lot of back-and-forth between the test class and the production class. We code a little bit of test (maybe a single assert), run the JUnit test suite to demonstrate failure, code just enough to production test, run the JUnit test suite to demonstrate success, clean up the code, run the JUnit test suite to ensure that nothing got broken. We repeat that cycle a few times. It sounds like a lot of work, but it’s not. I spent less than three minutes to get the code above in place.

Following the rule of TDD—we don’t build any more than our tests currently specify—we moved through the following steps:

Limiting the Class with Tests

Now we want to ensure that we can add multiple players, up to the maximum of 10 allowed in a typical Texas Hold ’Em game (see Listing 3).

Listing 3 Support for multiple players.

public void testAddMultiplePlayers() {
  for (int i = 0; i < Game.CAPACITY; i++)
   game.add("" + i);
  List<String> players = game.getPlayers();
  assertEquals(Game.CAPACITY, players.size());
  assertEquals("0", players.get(0));
  ...
}

We add the following line to Game in order to get testAddMultiplePlayers to compile:

public static final int CAPACITY = 10;

Do we really want to specify each and every number from one through the capacity in the test? J2SE 5.0 gives us a simpler way of constructing the test (see Listing 4).

Listing 4 Adding a test helper method.

public void testAddMultiplePlayers() {
  for (int i = 0; i < Game.CAPACITY; i++)
   game.add("" + i);
  assertPlayers("0", "1", "2", "3", "4", "5", "6", "7", "8", "9");
}

private void assertPlayers(String... expected) {
  assertEquals(expected.length, game.players().size());
  int i = 0;
  for (String player: game.players())
   assertEquals(expected[i++], player);
}

We can now improve the readability of our other two tests, as shown in Listing 5.

Listing 5 Refactored GameTest code.

public void testCreate() {
  assertPlayers();
}

public void testAddSinglePlayer() {
  final String player = "Jeff";
  game.add(player);
  assertPlayers(player);
}

The new test, testAddMultiplePlayers, should automatically pass with no other changes to the Game class. Does that mean that we didn’t need the test? Well, first, it would have been possible to craft the code in a more incremental fashion, so that the test didn’t pass initially. Second, the addition of the test forced the introduction of a limit on the number of players. The test now documents that limit. Change the name of the test to reflect that specification:

public void testAddMaximumNumberOfPlayers() {
...

Do we need another test to ensure that no more than 10 players are added to a game? The answer helps to distinguish TDD from testing techniques, or from something like design by contract.

Right now, we’re in complete control of what happens with the Game class:

Why would we build a system that allows adding more than 10 players? When we get to the point of providing a user interface, we’ll design the interface so that the system can’t possibly allow adding an eleventh player. Of course, we’ll write tests to help build that constraint into the interface.

If this makes you feel uncomfortable, go ahead and write a test to show what happens when you add an eleventh player. Maybe it throws an exception. But I’m confident enough that I’m not going to build a system that poorly. I also have my tests to guide me—reading what the tests say, I know how many players I can add to a game. By omission, adding too many players is undefined, so I’d better not do that.

Take some time to reflect on the tests we’ve created. Grab a fellow coder. First show him or her the names of the tests. Do they describe the capabilities of the Game class well? Ask your coworker to paraphrase the tests out loud. Can your associate quickly read and comprehend the test methods?

It’s easy to crank out a lot of test code in a short time. Remember that the refactoring part of the TDD cycle doesn’t just apply to your production code. You must continually rework the tests to eliminate unnecessary duplication and to make them clearly express the specifications they represent. There’s a subtle balance here—it’s possible to over-refactor tests so that they don’t read clearly.

At Last, Here Comes the Code

Okay, we still haven’t made much progress. I’m going to stop writing this text for a bit, and just crank out code. I’ll turn on the timer so we have a rough idea of how productive we are using TDD.

Tic tock...

Twelve minutes later, I’ve built some of the support for dealing hole cards. At the start of each Texas Hold ’Em hand, each player is dealt two cards down; these are known as hole cards. Cards are always dealt one at a time in clockwise order, starting with the player to the left of the dealer (marked by a button).

Before, we represented players by just a name. Dealing cards to players now requires a Player class to capture each player’s hand.

The twelve minutes of code presented in Listings 6–9 (GameTest, Game, PlayerTest, and Player) was all written test-first, of course, and was all refactored for readability and succinctness.

Listing 6 GameTest.

package domain;

import java.util.*;

import junit.framework.*;

public class GameTest extends TestCase {
  private Game game;
  private Deck deck;

  protected void setUp() {
   game = new Game();
   deck = game.deck();
  }

  public void testCreate() {
   assertPlayers();
  }

  public void testAddSinglePlayer() {
   final String name = "Jeff";
   game.add(new Player(name));
   assertPlayers(name);
  }

  public void testAddMaximumNumberOfPlayers() {
   for (int i = 0; i < Game.CAPACITY; i++)
     game.add(new Player("" + i));
   assertPlayers("0", "1", "2", "3", "4", "5", "6", "7", "8", "9");
  }

  public void testDealHoleCards() {
   Player player1 = new Player("a");
   Player player2 = new Player("b");
   game.add(player1);
   game.add(player2);

   game.dealHoleCards();

   assertHoleCards(deck, player1, 2);
   assertHoleCards(deck, player2, 2);
  }

  private void assertHoleCards(Deck deck, Player player, int expected) {
   List<Card> holeCards = player.holeCards();
   assertEquals(expected, holeCards.size());
   assertCardsDealt(deck, holeCards);
  }

  private void assertCardsDealt(Deck deck, List<Card> cards) {
   for (Card card: cards)
     assertFalse(deck.contains(card.getRank(), card.getSuit()));
  }

  private void assertPlayers(String... expected) {
   assertEquals(expected.length, game.players().size());
   int i = 0;
   for (Player player: game.players())
     assertEquals(expected[i++], player.getName());
  }
}

Listing 7 Game.

package domain;

import java.util.*;

public class Game {
  public static final int CAPACITY = 10;
  private List<Player> players = new ArrayList<Player>();
  private Deck deck = new Deck();

  public List<Player> players() {
   return players;
  }

  public void dealHoleCards() {
   for (int i = 0; i < 2; i++)
     for (Player player: players)
      player.dealToHole(deck.deal());
  }

  public void add(Player player) {
   players.add(player);
  }

  // needed for testing
  Deck deck() {
   return deck;
  }
}

Listing 8 PlayerTest.

package domain;

import java.util.*;

import junit.framework.*;

public class PlayerTest extends TestCase {
  private final static String NAME = "x";
  private Player player;

  protected void setUp() {
   player = new Player(NAME);
  }

  public void testCreate() {
   assertEquals(NAME, player.getName());
  }

  public void testHoleCards() {
   Card card1 = new Card(Rank.ace, Suit.hearts);
   Card card2 = new Card(Rank.three, Suit.diamonds);
   player.dealToHole(card1);
   player.dealToHole(card2);
   List<Card> holeCards = player.holeCards();
   assertCards(holeCards, card1, card2);
  }

  private void assertCards(List<Card> holeCards, Card... cards) {
   assertEquals(cards.length, holeCards.size());
   int i = 0;
   for (Card card: cards)
     assertEquals(card, holeCards.get(i++));
  }
}

Listing 9 Player.

package domain;

import java.util.*;

public class Player {
  private String name;
  private List<Card> holeCards = new ArrayList<Card>();

  public Player(String name) {
   this.name = name;
  }

  public String getName() {
   return name;
  }

  public List<Card> holeCards() {
   return holeCards;
  }

  public void dealToHole(Card card) {
   holeCards.add(card);
  }
}

I think I hear you saying, "Twelve minutes? I could have coded what’s in Player and Game in about six minutes." Maybe you could have, and maybe so could I. But you wouldn’t have ended up with comprehensive tests that demonstrate to other developers how to use the classes. You also might have made a simple mistake along the way—one that quickly bloated your time past twelve minutes while you scratched your head figuring out what went wrong.

I drove out those twelve minutes of code like a slow-and-steady tortoise beating out the sprinting hare. I can keep up this consistent pace for hours, days, months, getting feedback every few seconds that I’m on the right track. Many times when I’ve sprinted, I’ve had to retrace my route when I discovered that I wasn’t where I thought I should be. I used to program in bursts of a few hours’ worth of code without testing feedback. Often, I would find a defect that I had unwittingly introduced in the first half-hour of coding. I would then spend as much time unraveling that clump of code as I had spent keying it in the first place.

I’m not sure that twelve minutes qualifies as "banging out code." I’m going to go offline now and spend a bit more time, maybe an hour or two, really trying to put some meat into the Texas Hold ’Em game. I’ll summarize in the next installment what I produce.

Next segment: "Adding Some Bulk." Meanwhile, here’s the code (source.zip) we’ve built in this installment.

800 East 96th Street, Indianapolis, Indiana 46240