LESSON 0003 · Making code testable · Blackjack Ensemble course
Lesson 0003 — The whole point of the class
A game with a shuffled deck is random — and random code is hard to test. This repo removes the randomness with four reusable patterns. Learn them and every future test becomes a two-line setup.
StubDeck, build a game in any state with GameBuilder, assert with readable custom matchers, and get sentence-style test reports. This is the toolkit the ensemble reaches for every session.
The real deck is OrderedDeck shuffled by a Shuffler. If a test dealt from a random deck, you couldn't assert “the player has 20” — you'd get different cards each run. The fix is dependency substitution: Deck is an interface, so a test can pass a fake.
public interface Deck {
int size();
Card draw();
}
StubDeck — cards in the exact order you sayStubDeck deck = new StubDeck(Rank.ACE, Rank.THREE, Rank.EIGHT, Rank.TEN);
// draw() returns Ace, then Three, then Eight, then Ten — deterministic.
Now a test can arrange a precise scenario. The suit is a dummy (HEARTS) because most rules only care about rank.
StubDeckBuilder — decks that describe a scenarioBlackjack deals round-robin (player, dealer, player, dealer…), so hand-ordering raw ranks is error-prone. StubDeckBuilder lets you say what you mean:
StubDeck deck = StubDeckBuilder.playerCountOf(1)
.addPlayerHitsAndGoesBust()
.buildWithDealerDoesNotDrawCards();
It lays the cards out in true deal order for you — initial deal interleaved, then each player's “hit” cards, then the dealer's tail. Named helpers (addPlayerDealtBlackjack(), addPlayerHitsOnceDoesNotBust()) read like a sentence.
GameBuilder — a game already in the state you needMost tests don't want to wire up players, bets, shoe, and initial deal by hand. GameBuilder is a test data builder:
Game game = GameBuilder.createOnePlayerGamePlaceBetsInitialDeal(deck);
// players added, default bets placed, initial deal done — ready to act on.
Fluent form too: playerCountOf(2).withDefaultPlayers().deck(d).placeDefaultBets().initialDeal().build(). Builders keep each test to its essential three lines: arrange the scenario, act, assert.
Two finishing touches you'll see everywhere:
| Pattern | What you get |
|---|---|
| assertThat(game)… | Domain-specific matchers (GameAssert, HandAssert, CardAssert) generated by the assertj-assertions-generator Maven plugin — assertions that read in the domain's language. |
| ReplaceCamelCase | A JUnit DisplayNameGenerator: handWithOneAce… prints as “Hand with one ace…”. Test reports become a readable spec. |
And the base assertion library is AssertJ — fluent assertThat(x).isEqualTo(y), assertThatThrownBy(...), assertThatIllegalStateException(). Mockito's spy() stands in for ports like GameRepository when a test only needs to verify a call happened.
a. Why can a test pass a StubDeck where the game expects a Deck?
Deck is an interface
StubDeck implements the Deck contract, so it substitutes anywhere a Deck is needed — no randomness.
b. What does a test data builder like GameBuilder buy you?
short, intention-revealing setup
It hides irrelevant wiring so each test shows only what's essential to that scenario.
c. Which tool turns playerBusts into “Player busts” in the test report?
ReplaceCamelCase
It's a JUnit 5 DisplayNameGenerator that inserts spaces at camel-case boundaries.
domain test package · red → green
Write a test that proves a specific dealt hand busts — using a stub deck so the outcome is certain.
1 · Red. In a domain test class (e.g. a new MyDeckPracticeTest), arrange a one-player game where the player is dealt cards that clearly bust, then assert it. Sketch:
@Test
void playerDealtTwentyTwoIsBusted() {
StubDeck deck = StubDeckBuilder.playerCountOf(1)
.addPlayerHitsAndGoesBust() // JACK, NINE, then FOUR on hit
.buildWithDealerDoesNotDrawCards();
Game game = GameBuilder.createOnePlayerGamePlaceBetsInitialDeal(deck);
game.playerHits(); // draws the FOUR → 10+9+4 = 23
// assert the player's hand is busted — find the right query on Game/Hand
}
2 · Green. Run it (⌃⇧R). Use ⌘B on Game to discover the method that tells you the current player busted (hint: look for something built on isBusted() / player outcome). Make the assertion pass.
Predict before running: after addPlayerHitsAndGoesBust() deals JACK then NINE (=19), and playerHits() draws FOUR, what's the hand value?
23 — busted
10 (Jack) + 9 (Nine) + 4 (Four) = 23 > 21. No Ace, so no soft adjustment.
Say “done” — I'll open your test, run it, and check whether you found the cleanest query on Game. Then 0004 puts these patterns to work on a real red-green-refactor feature.
Ted Young on test doubles and builders: tedyoung.me. On stubs vs mocks, Martin Fowler's Mocks Aren't Stubs. AssertJ: the AssertJ docs. JUnit 5 display names: the User Guide.
Want the full menu of StubDeckBuilder and GameBuilder factory methods, or to see how a two-player scenario is built? Ask me — I'll list them from the real source.