LESSON 0003 · Making code testable · Blackjack Ensemble course

Lesson 0003 — The whole point of the class

Testability Patterns

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.

The win You'll control exactly which cards get dealt with a 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.

1 · The problem: randomness

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();
}

2 · StubDeck — cards in the exact order you say

StubDeck 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.

3 · StubDeckBuilder — decks that describe a scenario

Blackjack 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.

4 · GameBuilder — a game already in the state you need

Most 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.

5 · Custom AssertJ assertions & readable names

Two finishing touches you'll see everywhere:

PatternWhat 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.
ReplaceCamelCaseA 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.

6 · Quick recall

a. Why can a test pass a StubDeck where the game expects a Deck?

reveal

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?

reveal

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?

reveal

ReplaceCamelCase

It's a JUnit 5 DisplayNameGenerator that inserts spaces at camel-case boundaries.

7 · Hands-on — a scenario test, test-first (I'll check it live)

Task · new test in the 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?

reveal

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.

Primary source

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.