Intro to Test Driven Development (TDD) Part 1

Recently at a local meetup, I was asked to work on a kata with a few new guests who weren’t familiar with TDD. While I know what TDD is, and have a descent understanding of how to apply it, what I didn’t realize is how bad of a teacher I would be. I decided to write this article for a couple reasons – first, to send off to the new guests so they can get a better grasp of TDD, and second, so I can better understand it myself (and hopefully be a better teacher in the future).

What is Test Driven Development?

Test Driven Development is the practice of writing very small, specific test cases that mimic the requirements of the program. Test cases are written first, followed by writing the minimal amount code required to make the test pass, followed by refactoring the code you just created. The process is repeated, creating an iterative process that helps you build out your program and have confidence that it will work as expected. The purpose of this post is to demonstrate the process and help you understand it more.

The Kata

For this example, we’ll be working with C# (.NET Core / xUnit) and using the “Greed Kata” (you can find it, as well as other great katas at https://github.com/ardalis/kata-catalog), but I’ve included it here for reference (we’ll save the extra credit portion for part 2):

Instructions

Greed is a press-your-luck dice rolling game. In the game, the player rolls the dice and tries to earn as many points as possible from the result. For the purposes of this kata, we will just be scoring a single roll of five dice (but see Extra Credit below).

Write a scoring method that calculates the best score based on a given roll using the following set of scoring rules. Each die can only be scored once (so single die scores cannot be combined with triple die scores for the same individual die, but for instance four 5s could count as 1 Triple (500) and 1 Single (50) for a total of 550).

  • A single one (100 points)
  • A single five (50 points)
  • Triple ones [1,1,1] (1000 points)
  • Triple twos [2,2,2] (200 points)
  • Triple threes [3,3,3] (300 points)
  • Triple fours [4,4,4] (400 points)
  • Triple fives [5,5,5] (500 points)
  • Triple sixes [6,6,6] (600 points)

Example Scores

  • [1,1,1,5,1] = 1150 points
  • [2,3,4,6,2] = 0 points
  • [3,4,5,3,3] = 350 points

 

Step 1

The kata tells us that we need to create a scoring method, and also gives us the expected point total for each combination. Let’s start with the first rule about rolling a single one (if you’re coding along, you might notice that no parameter is being passed into the Score() method – we’ll get to that later):

We’ve given this test a very clear name that explains what the test is checking (Returns100WhenSingleOneIsRolled). For test names, I like to follow the advice given in https://ardalis.com/unit-test-naming-convention.

In this case, the Assert.Equal method is verifying that the score returned is 100 when a single 1 is thrown.

In order to make this test run, we must create a score method inside of our game class (you may want to try coding the methods first before looking ahead):

If you’re coding along, give this test a run. It will obviously fail because it is returning a value of 0. It’s important to first see the test fail before you make it pass – doing so lets you know that the test works by also failing when it should.

Step 1: failing test

Let’s make it pass now:

If we run the test now, it will pass.

Step 1: passing test

Congrats! Let’s move on to step 2.

Step 2

If we follow the scoring rules in order, the next one is to check that rolling a single 5 returns 50 points. Here’s what the test might look like:

Now, if we run our tests, the first one should still pass, but now this new one will fail. Instead of returning a value of 50, it returned a value of 100.

Step 2: failing test

It looks like we’ll need to fix our Score method to accept a parameter and return the proper score:

If you try to compile this now you might notice errors on the tests since we changed the signature of the Score method. Let’s fix them by adding the value of the die rolled as a parameter to the Score() method:

Both tests should now pass! On to step 3.

Step 3

Continuing along, let’s write a test to score triple ones:

Let’s now add a ScoreTripleOnes method that will initially fail. We’ll eventually want this as part of the score/calculate method, but I’ve decided to put it in its own public method to make it easy to test (we can refactor it once we have it working):

And to fix it:

Simple enough (for now)… Let’s try testing the score if 4 ones are rolled.

Step 4

So right now we know that if we roll three ones we get 1000, and if we roll a single one, we get 100. But we currently have no way to combine the value of both results. Let’s make a new test:

And the CalculateScore() method:

Run the new test and it will obviously fail. We need to combine the values of each die, but also need to make sure that an individual die is not counted more than once (in the case here, we would want to count the triple ones as one score at 1000, and the left over one at 100). Using something like a Dictionary can help this process. Let’s update the CalculateScore method like so:

This code is doing several things. First, it’s populating the dieCount dictionary, then it’s calculating the value of triple ones, then it’s calculating the value of a single die. That’s a lot of responsibility for one method, though. Since all of our tests have passed, we can start refactoring the CalculateScore() method.

Step 4: all tests passing

Step 5

Let’s refactor the CalculateScore() method to make it easier to read and understand:

Now let’s add a PopulateDieCounts() method. In order for these classes to work, we’ll also need a private class variable that all of the methods can share (_dieCounts).

Then the ScoreTripleOnes() method:

And finally, the ScoreSingleDie() method:

If you run your tests again you may notice a couple things. First, one of them fails.

Second, you may notice that we should update the test assertions to use the new CalculateScore() method, which will help us clean up our Game class as well. The updated test class should look like this:

We can also remove the Score() method from our Game() class since it’s no longer being used. Let’s remove that. The updated Game() class should look like this:

Run your tests now and they should all pass.

Step 5: all tests pass

Step 6

One thing we haven’t tested for yet is the value of triples of other numbers. Let’s use something that xUnit calls a Theory. A Theory lets your write one test, but pass in various sets of data (defined in a parameter called InlineData) allowing the test runner to execute one “test” for each set of InlineData:

If you look at the test method’s signature, you’ll see that it’s accepting two parameters (dieValue, and expectedScore). These values are passed in from the InlineData. So for example, on the first test it will pass in a dieValue of 2, and an expectedScore of 200, and run the test accordingly.

Run the test and you should see 5 failing tests.

Step 6: failing theories

There currently isn’t a method to calculate values of triple others. Let’s add one:

We’ll also have to update the CalculateScore() method to add ScoreTripleOthers to the total score:

Run your tests now, and they should pass! But…this code is ugly. Let’s refactor it real quick. Since the scoring formula for triple 2 through 6 is the same, this should be fairly simple:

Run the tests again, and they should still pass. Success!

Step 7

We’ve verified that scoring a single one, single 5, and triple scores work. Let’s try some other scores (for example, rolling 1,1,1,5,1 should return 1,150). Let’s start with a test for it:

Running the test appears to pass without having to change any code. Excellent! Let’s make a few more scenarios:

Oh no, one of the tests failed!

Step 7: failing test

The test Returns600GivenAllFives returned 250 instead of 600. Let’s look at the code for calculating ScoreTripleOthers() again. It looks like we’ve got a bug! Inside the if statement, we’re checking that there are 3 and exactly 3 of a given value. Really, we should be checking that there are at *least* three of a given value. Let’s fix it:

If the test is run again, it should now pass. At this point, we have successfully accounted for all of the scenarios given by the kata!

You may have also noticed that as the tests get more specific, the code gets more generic. This is a natural progression of TDD which “Uncle Bob” Martin explains in the linked article.

You can add more tests if you’d like (for example, maybe throw an exception if more than 5 dice are rolled, or if no dice are rolled).

Before we get to the final code, there’s one more bit of refactoring we can do on the test class.

Step 8

If you look at all of the tests, you’ll notice that each test is “newing up” a Game() instance. If there are only a couple of tests that’s ok, but once you get past 3 it’s not a bad idea to refactor it into field on the test class. The reason is fairly straightforward: if the Game() class’s constructor ever changes, we only have to update it once, vs. N times for N tests:

Then, in each class, you can remove the var game = new Game() line, and replace all instances of “game” with “_game”. You can see this in the final result.

Final Result

Your final code should look similar to this (download it here):

Test Class
Game Class

About the Author

FranksBrain

Frank is an experienced IT director of 15+ years who has been working with computers since the age of 13. While hardware and networking were the base of his career, he also enjoys programming, and is currently focused on .NET Core, Angular, and Blazor. He enjoys constantly learning something new, and helping others do the same.

1 thought on “Intro to Test Driven Development (TDD) Part 1

    • Author gravatar

      Years back I landed on a slight variation on test method naming conventions. I found it much, much easier to read tests like sentences (BDD) if I used underscores rather than camelcase. So “Returns50WhenSingleFiveIsRolled” becomes Returns_50_When_Single_Five_Is_Rolled. I tend to go lowercase, but I think the more important issue is what value we’re getting from our conventions. If this was standard source code (not test code), a really lengthy method name might be a code smell. A potential violation of SRP, if you will. The code convention helps police good practices by virtue of it’s readability and utility in that context. Tests are a slight different animal. We often capture broader intent in a single method, and for good reason. Complex interactions are often the point of what we’re doing, as well as the most likely opportunities for introducing bugs. Sometimes our conventions should be flexible enough to reflect these divergent motivations between test and source code.

Leave a Reply

Your email address will not be published.