Unit-testing – TDD a card game – where to start

tddunit testing

One of the most important rule in TDD is to write the simplest test to implement the simplest code in order to get closer to the end of a project.

I'd like to implement the game of Briscola.
It's a simple card game, with 40 cards, 2-5 players, and simple rules.

To start, I thought the best thing to implement was the core of the game, playing a hand.

To play a hand I need at least 2 players in a game, both will have 3 cards in hand, both have to be able to play one card each, and the game has to be able to decide the winner, and count points.

That looks like a lot of things to implement before passing that first test.

Should I still write that test down, and use it to implement everything needed ?
Or should I write tests for everything before trying to make that test pass ?

One thing that makes me reluctant to write a test as simple as "an_ace_is_worth_11_points" is that I'm afraid the design I'd start implementing would absolutely not fit the design to make a more concrete and complete test pass.

Best Answer

Because you're running too fast. When reading specifications you should stop as soon as you can write some code (after you understood the domain). I usually read the domain model specifications few times until I think I understood them and then I read them again using an highlighter to mark important bits to start coding.

To start, I thought the best thing to implement was the core of the game, playing a hand. To play a hand I need at least 2 players in a game, both will have 3 cards in hand, both have to be able to play one card each, and the game has to be able to decide the winner, and count points.

First of all write down the complete specifications for this game. Everything, even if you think you have it clear in your mind. It doesn't even need to be strictly formal.

Let's go through this word by word, the first keyword to highlight is game. You will, probably, then need a Game class you want to create. Let's write a test method (syntax is for C# but the important part is the concept):

[TestMethod]
void CanCreateNewGame()
{
    var game = new Game();
}

So far so good, we just want to be sure we can create a Game object and it doesn't throw any exception. If we will introduce a factory (for example to join an on-line pending game) then this is the test we need to rewrite. Let's write code to make it pass:

public sealed class Game { }

Now continue scanning your specifications, you have players. You will then need to create them, same as above. You see then ...in a game.... It means they must be stored in the Game class and they can be from 2 to 5, you need also a collection.

[TestMethod]
public void CanCreatePlayer()
{
    var game = new Game();
    game.Players.Add(new Player());
}

Let's declare the class and its collection PlayerCollection then add it to the game class:

public sealed class Player { }
public sealed class PlayerCollection : Collection<Player> { }

public sealed class Game
{
    public PlayerCollection Players { get; } = new PlayerCollection();
}

Our tests are green, we can now move to the next important part (the number of players). When we need at least two players and no more than 5? When we start a game:

[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void CannotStartGameWithLessThan2Players()
{
    var game = new Game();
    game.Start();
}

Make it compile, add Start() to Game:

public void Start()
{
    if (Players.Count < MinimumNumberOfPlayers || Players.Count > MaximumNumberOfPlayers)
        throw new InvalidOperationException("...");
}

Just continue like this until your application is completed. Now you can move that check into a separate function EnsureThereIsRightNumberOfPlayers() and you already have all the tests to do it safely. I'd suggest to also add assertions to help you with tests (countless times an assertion failed when running my tests and I found I had to add more tests...) but it's probably outside the scope of your question.

Writing tests before code will also help to have a better code coverage and to keep your code testable even when you just started using unit testing.


What I shown above is a literal TDD approach, I am often little bit more pragmatic and I write bigger code blocks and/or multiple tests in one shot. For example I will NEVER write int Sum(int a, int b) => 2; because I need to pass the test Assert.AreEqual(2, Sum(1, 1,)) (to fix it in the next test when I need to verify Sum(-1, -1)). I'd write, whenever possible, enough code to pass the test but using what may be a true implementation (which I may refine and refactor later because I already have tests in-place).

With some experience with TDD you will find what's the best compromise for you.


TDD as a tool to study the domain

Note: TDD won't replace design phase, it's not its role. It will, however, help you to refine your design from an abstract domain point of view to a practical implementation. Also note that the type of tests you write in this case is pretty different from the type of tests you write on a normal TDD approach.

Sometimes the domain isn't perfectly clear and you need to write some code to outline overall architecture. This case, IMO, is when you have greater benefits with TDD. Didn't ever happen to write a great model which is a pain to consume? If you start consuming your model first then this won't ever happen.

In this case I find convenient to write a slightly complex test method to understand who the actors are, for example a very naive first approach to the problem may be:

[TestMethod]
public void CanSetupGame()
{
    // I have a game
    var game = new Game();

    // With at least two named players
    game.Players.Add(new Player("Adam"));
    game.Players.Add(new Player("Jon"));

    // I can start the game
    game.Start();

    // And I keep playing until there are cards in the table...
    while (game.Cards.Count > 0)
    {
        // Print current cards to...console?
        var availableCards = game.CurrentPlayer.Cards;

        // Read user input from?
        var selectedCard = availableCards.First();

        game.CurrentPlayer.Play(selectedCard);
    }
}

As you can see you immediately see what your objects have to do, you need to keep track of the current player, its cards and stop playing when...cards are finished. You also immediately see that more users may share the same screen and so on...

Of course you will need to write a lot of code before this test can pass (you may temporarily [Ignore] it) and it MUST change/evolve over time but it will be your GPS to write all the other smaller tests you need. You may even find that some of these "high level abstraction" tests should be moved to integration tests, for example. Even better. Note that you should not throw away these tests, they're great self-documenting examples and the first thing that someone else should see when studying your code.

Writing this kind of rough code will greatly help you to better understand the Big Picture before writing one single line in your model (and some IDEs let you generate stubs from this code...). After you wrote above code you may then think "Shouldn't everything be inside Start()?". You will then drop that while and you will next think "How should I get inputs?" and add, for example, an Input property of type Stream to the Game class. You will do the same for Output and you will then ask yourself "What if I want to make it graphical?" and replace it with with a UserInteraction class. Keep changing, refining and increasing your understanding. You won't write a model (and its tests) you need to throw away because it's a pain to use (which may also cause the calling code to be a mess because you don't want to throw away such beautiful already written model...)

Note that this consumer to implementation approach will also help you to refine some of your architectural decisions, for example:

  • Is the language a good fit to express the model? Should I move to another language? To a DSL?
  • Do I need any framework? Are frameworks I supposed to use good for this job? Did they drive in the choice of the language?
  • Are performance good enough? Should I review my algorithms?
  • This package/module/class depend on the other one to perform its task. Is it a good enough decision or should I move responsibilities?
  • Did I practically put too many responsibilities on this class and I'd better move them out? Is there a chance to reuse this code somewhere else?

You probably have already noted that DDD, TDD and - when dealing with UI - lean UX design aren't separate steps or different approaches (!!!) but they go pretty well all together.

Related Topic