A Solution
So how are we going to solve the problem of how to test shuffling? I’d just as soon avoid mocking. Don’t worry, though—we’ll visit mock techniques later in this series. If confidence and specification of behavior is all I’m looking for, we can use a variant of the first technique I suggested.
Suppose we assume that the deck is shuffled on creation. We create two decks, deal all cards from each deck, and compare the two sequences of cards dealt. Sure, it’s possible for the two decks to be shuffled exactly the same way, but it’s extremely unlikely, and might never happen in my lifetime. I’m okay with that risk—it’s not as bad as tests failing once every 52 or so times, which is unacceptable.
What this test would prove is that the Deck class shuffles the cards. How well it shuffles the cards is another matter. If we need to prove that other matter, we can write tests specifically against the random number generator. Just remember that proving the random number generator is not our job in writing tests for Deck. (If you’re interested in some of the math and some of the worries behind shuffling, check out this interesting discussion at Paradise Poker.com.)
Let’s code it. First, we’ll need a test that proves we can simply deal cards, regardless of whether the deck is shuffled (see testDeal in Listing 1). I refactored DeckTest to use a common Deck object that gets initialized in setUp.
Listing 1 Specifying the deal in DeckTest.
package domain; import junit.framework.*; public class DeckTest extends TestCase { private Deck deck; protected void setUp() { deck = new Deck(); } public void testCreate() { assertEquals(Deck.SIZE, deck.cardsRemaining()); for (Suit suit: Suit.values()) for (Rank rank: Rank.values()) assertTrue(deck.contains(rank, suit)); } public void testDeal() { for (int i = Deck.SIZE; i > 0; i--) { Card card = deck.deal(); assertEquals(i - 1, deck.cardsRemaining()); assertFalse(deck.contains(card.getRank(), card.getSuit())); } assertEquals(0, deck.cardsRemaining()); } }
I’ve highlighted the code in Deck that differs from last month’s version (see Listing 2). I’m not sure I like the deal method implementation much, but it does work. Now that we’re dealing, writing the method to test shuffling is simple (see Listing 3).
Listing 2 Initializing the object in Deck.
package domain; import java.util.*; public class Deck { public static final int SIZE = 52; private Set<Card> cards = new HashSet<Card>(); private Iterator<Card> it; public Deck() { for (Suit suit: Suit.values()) for (Rank rank: Rank.values()) cards.add(new Card(rank, suit)); } public int cardsRemaining() { return cards.size(); } public boolean contains(Rank rank, Suit suit) { return cards.contains(new Card(rank, suit)); } public Card deal() { if (it == null) it = cards.iterator(); Card card = it.next(); it.remove(); return card; } }
Listing 3 Are you ready to shuffle?
public void testDeckShuffledOnCreation() { Deck deck1 = new Deck(); Deck deck2 = new Deck(); List<Card> hand1 = new ArrayList<Card>(Deck.SIZE); List<Card> hand2 = new ArrayList<Card>(Deck.SIZE); for (int i = Deck.SIZE; i > 0; i--) { hand1.add(deck1.deal()); hand2.add(deck2.deal()); } assertFalse(hand1.equals(hand2)); }
We need to run this test to ensure that it fails; then we can make the following changes to Deck in order to make it pass:
private List<Card> cards = new ArrayList<Card>(); ... public Deck() { for (Suit suit: Suit.values()) for (Rank rank: Rank.values()) cards.add(new Card(rank, suit)); Collections.shuffle(cards); }
As I mentioned earlier, the shuffle method expects a List reference. Our implementation of Deck doesn’t care. We can change Deck to use a List instead of a Set and not break any tests. Don’t forget, though, that we need to refactor the new test code and eliminate duplication with the other Deck tests! (Downloadable code will show this refactoring.)
Next segment: "Dealing with Exceptions." Meanwhile, here’s the code (source.zip) we’ve built in this installment.