Tic-Tac-Tutorial: Test-Driven Development

Test-Driven Design is a big buzzword these days – but what does it really mean?

Martin Fowler explains it thus:

Test-Driven Development (TDD) is a technique for building software that guides software development by writing tests. It was developed by Kent Beck in the late 1990’s as part of Extreme Programming. In essence you follow three simple steps repeatedly:

1. Write a test for the next bit of functionality you want to add.
2. Write the functional code until the test passes.
3. Refactor both new and old code to make it well structured.

You continue cycling through these three steps, one test at a time, building up the functionality of the system. Writing the test first, what XPE2 calls Test-First Programming, provides two main benefits. Most obviously it’s a way to get Self-Testing Code, since you can only write some functional code in response to making a test pass. The second benefit is that thinking about the test first forces you to think about the interface to the code first. This focus on interface and how you use a class helps you separate interface from implementation.

If you want further elaboration, check the wiki.

After the last installment, you’ll recall we defined our entities (classes and their properties), but we didn’t write any actual functionality. That’s because we want to write our tests first, then work on the implementation.

Download the source code to follow along.

As Fowler notes, writing the tests first forces us to think about the interface of the code – that is, it forces us to think about it from a usability perspective. I can’t tell you how many times I wrote something that ended up being hard to use because while I knew what the code should be doing, I didn’t think first about how the code should be used. Also note that Fowler says “the next bit of functionality” – that means these tests, and the code that’s being implemented, should be very granular and very specific, and not have too large of a scope.

On the other hand, eventually we will need to have larger-scoped tests. These will test the big picture, for the overall goals we want to accomplish. This is a different kind of test (which i’ll get into in a later installment) but ultimately the most important. If we were building a bicycle, it’s great to know that the tires are inflated but if it can’t get you from start to finish then the whole thing is a failure.

Let’s write some tests

First, we’ll create a Unit Test project – I like to use the naming convention {project-to-test}.Tests, so we’ll call this project TicTacToeCore.Tests.

Would You Like To Play A Game?

Let’s start a new game.

We’ll need to get a new Game instance, with some Players, an empty board.

But we weren’t not going to do this manually every time, so we’ll probably have some other class do this for us. A GameManager perhaps? Let’s go ahead and create GameManager.cs in the TicTacToeCore project, and a GameManagerTests.cs in the TicTacToeCore.Tests project.

So our test method will create a new game and then we’ll assert that the resulting object matches our expectations.

First, let’s just write a simple and straightforward test, because we can’t get very far just yet:

    [TestMethod]
    public void GameManager_CreateNew_should_return_a_new_game()
    {
        // Arrange
        var mgr = new GameManager();

        // Act
        var game = mgr.CreateNew();

        // Assert
        Assert.IsNotNull(game);
    }

The compiler doesn’t like the references to GameManager and CreateNew(), because they haven’t been created yet. Let’s do that next:

    public class GameManager
    {
        public Game CreateNew()
        {
            return null;
        }
    }

This is now the bare minimum of code needed to get everything to compile. The compiler is actually your very first test. Notice that i’m returning null here instead of going ahead and new-ing up a Game. It’s important that we witness the test start with a failure.

Go ahead and run the test to confirm it fails.

It’s always important to watch a test fail first, because otherwise you don’t have a positive confirmation that the code you just wrote makes the test pass – if you don’t confirm the test changing from fail to pass, then all you really proved is that nothing failed. A non-failure is not the same thing as a positive confirmation that your new code works.

Now let’s make it pass! Instead of returning null, return a new Game.

    public class GameManager
    {
        public Game CreateNew()
        {
            return new Game();
        }
    }

Now re-run the test to confirm it passes.

Congratulations! You’ve just entered the world of Test-Driven Development.

The next step is to build on top of this successful test and continue to add new tests and new functionality:
2. Player1 is not null,
3. …its Mark is X,
4. …its Name is “Player 1”
5. Player2 is not null,
6. …its Mark is O,
7. …its Name is “Player 2”
8. PlayerTurn is “Player 1”
9. IsFinished is false
10. IsDraw is false
11. Winner is null
12. Board is not null, and every square property is null

If you’re dedicated to seeing your tests fail, then you’ll have some issues with tests 9 and 10 – because the default value of a bool is going to be false. So if you need to see it fail, you need to first initialize these properties to true, run the test and confirm failure, then remove that code and run the test and confirm success. Exercise caution when testing against default values, because you know what happens when you make assumptions. This includes false for bools, 0 for ints, null for references, etc.

You can implement all these tests as individual test methods, or as additional asserts in a single condensed test method (as long as they’re all related to a single testing aspect). There’s pros and cons to each style.

Here’s an example of a unit test style where there is a common setup and each assertion is its own method (uses the NBehave framework):

    [TestClass]
    public class when_using_GameManager_CreateNew : SpecBase
    {
        protected GameManager gameManager;
        protected Game game;

        protected override void Establish_context()
        {
            gameManager = new GameManager();
        }

        protected override void Because_of()
        {
            game = gameManager.CreateNew();
        }

        [TestMethod]
        public void then_it_should_return_a_new_instance()
        {
            Assert.IsNotNull(game, "game");
        }

        [TestMethod]
        public void then_Player1_should_be_initialized()
        {
            Assert.IsNotNull(game.Player1, "game.Player1");
            Assert.AreEqual('X', game.Player1.Mark, "game.Player1.Mark");
            Assert.AreEqual("Player 1", game.Player1.Name, "game.Player1.Name");
        }

        [TestMethod]
        public void then_Player2_should_be_initialized()
        {
            Assert.IsNotNull(game.Player2, "game.Player2");
            Assert.AreEqual('O', game.Player2.Mark, "game.Player2.Mark");
            Assert.AreEqual("Player 2", game.Player2.Name, "game.Player2.Name");
        }

        [TestMethod]
        public void then_PlayerTurn_should_be_Player_1()
        {
            Assert.AreEqual("Player 1", game.PlayerTurn, "game.PlayerTurn");
        }

        [TestMethod]
        public void then_IsFinished_should_be_false()
        {
            Assert.IsFalse(game.IsFinished, "game.IsFinished");
        }

        [TestMethod]
        public void then_IsDraw_should_be_false()
        {
            Assert.IsFalse(game.IsDraw, "game.IsDraw");
        }

        [TestMethod]
        public void then_Winner_should_be_null()
        {
            Assert.IsNull(game.Winner, "game.Winner");
        }

        [TestMethod]
        public void then_Board_should_be_initialized()
        {
            Assert.IsNotNull(game.Board, "game.Board");
            Assert.IsNull(game.Board.Square1, "game.Board.Square1");
            Assert.IsNull(game.Board.Square2, "game.Board.Square2");
            Assert.IsNull(game.Board.Square3, "game.Board.Square3");
            Assert.IsNull(game.Board.Square4, "game.Board.Square4");
            Assert.IsNull(game.Board.Square5, "game.Board.Square5");
            Assert.IsNull(game.Board.Square6, "game.Board.Square6");
            Assert.IsNull(game.Board.Square7, "game.Board.Square7");
            Assert.IsNull(game.Board.Square8, "game.Board.Square8");
            Assert.IsNull(game.Board.Square9, "game.Board.Square9");
        }
    }

And for comparison, here’s an example of a unit test style where the assertions are not so granular:

    [TestMethod]
    public void GameManager_CreateNew_should_return_a_new_game()
    {
        // Arrange
        var gameManager = new GameManager();

        // Act
        var game = gameManager.CreateNew();

        // Assert
        Assert.IsNotNull(game);
        Assert.IsNotNull(game.Player1, "game.Player1");
        Assert.AreEqual('X', game.Player1.Mark, "game.Player1.Mark");
        Assert.AreEqual("Player 1", game.Player1.Name, "game.Player1.Name");
        Assert.IsNotNull(game.Player2, "game.Player2");
        Assert.AreEqual('O', game.Player2.Mark, "game.Player2.Mark");
        Assert.AreEqual("Player 2", game.Player2.Name, "game.Player2.Name");
        Assert.AreEqual("Player 1", game.PlayerTurn, "game.PlayerTurn");
        Assert.IsFalse(game.IsFinished, "game.IsFinished");
        Assert.IsFalse(game.IsDraw, "game.IsDraw");
        Assert.IsNull(game.Winner, "game.Winner");
        Assert.IsNotNull(game.Board, "game.Board");
        Assert.IsNull(game.Board.Square1, "game.Board.Square1");
        Assert.IsNull(game.Board.Square2, "game.Board.Square2");
        Assert.IsNull(game.Board.Square3, "game.Board.Square3");
        Assert.IsNull(game.Board.Square4, "game.Board.Square4");
        Assert.IsNull(game.Board.Square5, "game.Board.Square5");
        Assert.IsNull(game.Board.Square6, "game.Board.Square6");
        Assert.IsNull(game.Board.Square7, "game.Board.Square7");
        Assert.IsNull(game.Board.Square8, "game.Board.Square8");
        Assert.IsNull(game.Board.Square9, "game.Board.Square9");
    }

The granular style is 77 lines, the condensed style is 32. That’s a lot of source-code overhead for the same exact tests. But readability is the most important thing, in code or in tests. Figure out what reads the easiest to you.

If you have individual tests it becomes easy to tell which aspect of the code has failed – but on the other hand, your test code is now a lot more verbose and that can be hard to read and keep track of. Play around with each style to see which one you prefer – but keep in mind that there are other factors, like the kind of application being written, contractual requirements, other developers’ preferences, etc.

Make sure that your assertion failure messages are detailed enough to quickly understand the underlying issue, without having to jump into the code. Over time, your test suite can easily (EASILY) balloon up to hundreds or thousands of tests. The test code base itself becomes something that must be managed. You’re not going to be able to remember all the details, so give yourself enough hints in the failure messages and you’ll thank yourself later.

From here, you should have a good idea of how to write tests and implement new code to actually play a game of Tic Tac Toe and to determine the winner. I’ll leave that up to you before we move on to the next installment of this series.

One thought on “Tic-Tac-Tutorial: Test-Driven Development

  1. Pingback: Tic-Tac-Tutorial Series | PhilChuang.com

Leave a Reply