Have fun learning about Test-Driven Development with JitterTed's TDD Game

Tightening Our Assertions

Part 4 of the Predictive Test-Driven Development series: looking at the precision of assertions, as well as taking smaller steps when doing TDD.


A blue vise clamp is squeezing a wooden block that has the word 'Assertions' written on it.
Really Squeeze 'Em

Specifying, or Over-specifying?

Last time, we were test-driving our Blackjack game. We got all the tests to pass, but there were questions around the precision of our assertion. Here’s the test we ended with:

@Test
public void playerWithTwoCardsDrawsCardResultsInThreeCards() {
Game game = new Game();
game.initialDeal();

game.playerDrawCardFromDeck();

assertThat(game.playerCards())
.hasSize(3);
}

Alternative Solution

Last time, our solution:

public void playerDrawCardFromDeck() {
playerHand.add(deck.draw());
}

was copied from the implementation in the initialDeal() method:

public void initialDeal() {
// player gets card first due to rules of Blackjack
playerHand.drawCardFrom(deck);
dealerHand.drawCardFrom(deck);

// everyone gets two cards, so deal another round
playerHand.drawCardFrom(deck);
dealerHand.drawCardFrom(deck);
}

What if we didn’t already have a copy-and-paste solution? What would’ve been the least effort implementation? How about:

public void playerDrawCardFromDeck() {
playerHand.add(null);
}

That would still get the test to pass, as there would be 3 entries in the player’s list of cards. How? Well, null counts as a card![1] This certainly isn’t what we want in our game, though. Perhaps we’re missing some assertions? Let’s look back at our original specification:

Given a PLAYER with a HAND containing 2 CARDs,

When the PLAYER DRAWs a CARD from the DECK,

Then the HAND should have 3 CARDs.

The highlighted part of “from the DECK” is something that we’re not asserting. We’re also not ensuring the cards in the player’s hand are valid instances, without any nulls. So let’s fix that.

Tightening Our Assertions

Sometimes “loose” assertions are fine, as they specify just enough of the behavior we need to get a failing test, like we did by asserting the hand had 3 things in it. Then we can use our professional judgment of the implemented code to determine if the assertions are appropriate.

In this case, we see that our “add null” implementation passes the test, but isn’t what we want[2]. Let’s adjust that by going through a “tightening phase”, ratcheting up the precision of the assertions one at a time. Note that each tightening step will still follow the Red-Green part of the TDD cycle.

Hand Should Have 3 (Non-Null) Cards

First, let’s make sure that what’s in the player’s list of cards are all actually instances of Card. We could examine each element to see if it’s an instance of Card, but since the list can only hold Card objects (yay generics), we can get away with just making sure there are no nulls in the list. Here we’ll use AssertJ’s handy doesNotContainNull() assertion[3]:

assertThat(game.playerCards())
.doesNotContainNull()
.hasSize(3);

Since we’ve changed what this test asserts, according to Predictive TDD, we need to state our failure prediction precisely:

This test will fail, because there is indeed a null in the list, and we assert there aren’t any.

java.lang.AssertionError:
Expecting actual:
[Card {suit=♠, rank=A}, Card {suit=♦, rank=2}, null]
not to contain null elements

Success! It fails as expected. Now let’s get the test to pass by writing the least effort code[4]:

public void playerDrawCardFromDeck() {
playerHand.add(new Card("♦", "8"));
}

Creating a Card out of thin air is allowed, and certainly requires little effort. We know it’ll be temporary, but if the test passes, then we know the test is doing its job enforcing the list to have no null object references.

We can now predict the test will pass, and it does.

Even Tighter: Card Comes From the Deck

Let’s make our assertions even more precise and enforce the requirement that the newly added Card actually comes from the Deck (no cheating by pulling a card out of your sleeve!). How do we do this? The Game code does not provide a way to directly access the Deck object, nor a way to pass it into its constructor[5]. This is not a test problem, but a code (possibly design) problem. At this point, we have three choices:

  1. Just change the implementation to take a Card from the Deck instead of instantiating a new Card.

  2. Add a new public query method that returns either:

    a) The Deck instance, where we can assert that its size has been reduced by 1, or,

    b) Just the size of the deck, since we don’t need any other information about the deck.

  3. Add a Game constructor that takes a Deck instance, which allows us the option of asserting .

Option 1: Just change it

This might seem like skipping a step, but we are allowed to use our professional judgment. We can look at the code, see that it would be correct, and make the change. It’s one line of code, and we’re replacing something “fake” (directly instantiating a Card) with something real (drawing the Card from a Deck). We don’t always have to have a failing test when doing something obvious (in our judgment) like this[6]. With this option, we would replace new Card("♦", "8") with deck.draw(), run the test (which would pass) and call it done.

Option 2: Add test-specific methods

Options 2a and 2b seem wrong, because we’d add public methods purely for testing. It might be appropriate as a stepping stone towards improving the overall design, but by itself it means the state is changing somewhere that we’re not able to observe. What’s worse, even with that information, we still wouldn’t know for sure that the Card drawn from the Deck was added to the player’s hand. The following implementation would make it seem (to an outside observer, like our test) that the right thing happened:

public void playerDrawCardFromDeck() {
deck.draw(); // reduces the number of cards in the Deck
playerHand.add(new Card("♦", "8")); // adds a different card
}

Again, we could use our professional judgment and decide we wouldn’t do something silly like that. So we could add a method to Game:

public int deckSize() {
return this.deck.size();
}

and assert in our test that this is the expected size, like this:

assertThat(game.deckSize())
.isEqualTo(52 - 4 - 1);
// 52: original size of the `Deck`
// subtract 4: 2 cards dealt each to player & dealer
// subtract 1: the player drew a card from the deck

However, providing a test-only query method like this is a code smell[7].

Option 3: Inject Deck into Game

Option 3 is ultimately the better way to go. We can provide Game with a Deck that we construct in our test, so that we know that the Card in the player’s hand is the one that came directly from the Deck.

We’ll see how to do this next time, leveraging some automated refactorings that IntelliJ IDEA provides. We’ll also dismantle our null-checking scaffolding once it’s done serving its purpose.


  1. Even though the hand is a genericized List that only holds Card instances, null is allowed. ↩︎

  2. And possibly not even a reasonable implementation, despite it being “least effort code”. ↩︎

  3. Interested in learning more about AssertJ’s powerful—and seemingly unending—set of assertions? Let me know on Twitter or on my Discord. ↩︎

  4. Using a “literal” is almost always the least effort code. I consider new Card("♦", "8") to be a literal, because it’s not computed and no logic is used in its creation, and the Suit and Rank that make it up are themselves literals. You could call it a constant, but I often associate that with a named constant, e.g. MAX_CACHE_SIZE. ↩︎

  5. That’s right, dependency injection, a fancy way to say “pass in an object as a parameter to the constructor”. ↩︎

  6. As GeePaw Hill says, “Always suspect ‘always’ and ‘never’”. ↩︎

  7. And no, adding some annotation like @VisibleForTesting on the deckSize() method does not help. We might do this as scaffolding (a temporary helping structure) to get us to somewhere better, but only if there’s no other way to go. ↩︎


Make Your Code More Testable™️

Don't miss out on ways to make your code more testable. Hear about new Refactoring techniques, improving the Test-Driven Development process, Hexagonal Architecture, and much more.

    We respect your privacy. Unsubscribe at any time.