I spend a lot of time teaching TDD to engineers. Some of my most rewarding sessions are spent with first-time TDD practitioners. When we're starting out, I'll choose a simple TDD kata, and we'll pair to solve it together.
Once I've introduced myself and the exercise, I always ask the same opening question: "What's the first test we should write?"
The answers are always interesting, because they tell me how the candidate thinks about TDD. If they have misconceptions, it's best to tackle them at the beginning. In my experience there are three common ways that people go wrong when choosing the first test.
On this page
Asserting the trivial
The most common mistake is to focus on testing things that are necessarily, or trivially, true. For example, in the Tic-Tac-Toe kata, the conversation might go like this:
What's the first test we should write?
I think I need to test that we can create a board
describe("when starting a game", () =>{
const board = new Board()
it("should exist", () => {
expect(board).not.toBeUndefined()
})
})
Similarly, sometimes beginners want to assert the type of their newly created object.
def test_can_create_board():
board = Board()
assert type(board) is Board
What's wrong with these tests?
These tests don't help us to specify our solution. Instead, they're testing that the programming language works as it should. The only thing that could fail this test is a compiler error.
I think the thought process runs: "I need to create a class, therefore I need to test that I create the class". The engineer is looking for a test that will cause them to write the code they've already decided on.
This is back to front, though. Instead, we want to write a test that specifies the behaviour of our code. The test will tell us what the design ought to be. We want to write the test as though our perfect code already existed, and then make it work.
One step up from testing the abstract concept of constructors is testing the specifics of our constructor.
describe("when creating a 3 x 3 board", () => {
const board = new Board(3, 3);
it("should be the right size", () => {
expect(board.height).toBe(3);
expect(board.width).toBe(3);
})
})
This is a little better, but still doesn't help us very much. Again, this test follows from the thought process "I need to create a board, and it needs to be 3x3". The test is focused on verifying data, but we ought to be checking behaviours instead.
What could we do differently?
When we're looking for our first test, we should focus on a behaviour that we want from the system. That behaviour shouldn't be something that's guaranteed by the programming language (the new
keyword returns an object) or something trivial (I set fields in the constructor). It should be something that moves us meaningfully forward. For example
describe("When starting a new game", () => {
const game = new Game();
it("should alternate moves between the players", () => {
expect(game.nextMove).toBe(Player.X);
game.play(0,0);
expect(game.nextMove).toBe(Player.Y)
})
})
This is simple to implement, but forces us to build a key mechanism of the game - players take it in turns to move. We don't have to build any logic for the win condition, or even for updating the board, we just have to pass play between X and O.
enum Player {
X,
O
}
class Game {
#nextMove: Player = Player.X;
public play(x: number, y: number) {
if(this.#nextMove == Player.X) {
this.#nextMove = Player.Y;
} else {
this.#nextMove = Player.X;
}
}
}
For our next test we might check that the board is updated when we play
it("should put the player's token on the board", () => {
expect(game.get(0, 0)).toBe(Player.X);
})
And now we're off and away!
Negative outlook
The second mistake I see in TDD first-timers is starting with an error case. For example, we might choose the Leap Year kata. In this kata, we're asked to build code that can answer, true or false, whether a given year is a leap year.
What's the first test that we should write?
I need to make sure that the year is a number.
describe("When the year is a string", () => {
expect(() => isLeapYear("poop")).toThrow();
})
I think this stems from engineers believing that tests are about "checking correctness". In traditional testing, we spend a lot of time looking for boundary cases - what happens when we use an empty string, a negative number, a string that's too long, and so on. The thought process is "The leap year function takes a number, I need to test that it can only take a number".
What's wrong with these tests?
There are an infinite number of things that our code does not do. isLeapYear
does not accept strings, it does not tell you whether a number is prime, it does not accept arbitrary numbers of arguments, it does not generate pictures of chimps, and so on.
We'll never get anywhere by declaring the things our code does not do. Instead, we want to choose a positive behaviour: a thing that our code does do.
test("a year is a leap year if divisible by 4", () => {
expect(isLeapYear(4)).toBeTrue();
expect(isLeapYear(16)).toBeTrue();
expect(isLeapYear(64)).toBeTrue();
expect(isLeapYear(4000000004)).toBeTrue();
})
const isLeapYear (n:number) => {
return n % 4 == 0
}
Our next test might introduce one of the exceptions to the rule.
test("a year is not a leap year if divisible by 400")
Choosing a positive behaviour helps us to move our code in the right direction. We can worry about error cases at the very end, if at all.
Biting off more than they can chew
The last mistake I see a lot is beginners being too ambitious in their choice of first test. For example, when practising domain modelling techniques, we might use the Poker Hands Kata. This kata asks us to play one round of poker, scoring each player's cards as follows:
What's the first test we should write?
I think I want to tackle "Full House"
describe("When a player has a full house", () => {
const player1 = ["2H", "2C", "2D", "3S", "3D"]
const player2 = ["5C", "4C", "QC", "KC", "9C"]
it("should beat a Flush", () => {
const game = new Poker();
expect(poker.play(player1, player2)).toEqual("Player 1 wins with Full House (3)");
}
})
Here the thinking is "If I tackle the most complex case first, that will flush out all the rest of the behaviour".
What's wrong with this test?
A test that requires us to build the whole system isn't very helpful. Think of the problem we're trying to solve as a mountain to be climbed. The mountain is far too big for us to walk up it in a single step. We need to break the problem down into manageable pieces.
Our tests are like a staircase that we carve into the mountainside. Each passing test lifts us one tiny step closer to the summit. If we lose our footing, we can always return to the last safe place - the last passing test - and try again.
This test asks us to:
- Parse card values like
9D
orQC
- Find groups of cards in a hand, like "pairs", or "triples", or sets of the same suit.
- Identify the highest ranked card in a hand
- Be able to identify that a full house comprises a pair and a triple
- Be able to identify that a flush comprises 5 cards of the same suit
- Write a rule that decides that a flush is worth less than a full house
- Output a well-formed string describing the winning player's hand.
That's a lot of mountain to be climbed!
What could we do instead?
Our first test should be the tiniest increment that moves us forward meaningfully.
describe("When comparing cards", () => {
const cards = [
[5, "spades"],
[3, "clubs"],
[9, "diamonds"],
[4, "hearts"],
];
it("should order them by rank", () => {
expect(cards.sort(compareCards)).toEqual([
[3, "clubs"],
[4, "hearts"],
[5, "spades"],
[9, "diamonds"],
]);
});
});
type Rank = number
type Suit = "diamonds" | "hearts" | "clubs" | "spades";
type Card = [rank: Rank, suit: Suit];
function compareCards(a: Card, b: Card) {
if (rank(a) > rank(b)) return 1;
if (rank(b) > rank(a)) return -1;
return 0;
}
const rank (c: Card) => return c[0];
This test establishes that cards have a rank and a suit, and that one card can be bigger than another, so that we can have a partial ordering over them.
From here we would probably handle the face cards like ["queen", "hearts"]
or ["ace", "spades"]
and then tackle a test like the hand with the highest card wins
.
Summary
The first test of our test suite will often set the direction that our code takes. It's important to choose something that sends us on a useful path.
The first test of any new suite should be the smallest positive behaviour that moves us meaningfully forward.