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.
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):
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).
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):
1 2 3 4 5 6 7 8 9 |
public class CalculateScore { [Fact] public void Returns100WhenSingleOneIsRolled() { var game = new Game(); Assert.Equal(100, game.Score()); } } |
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):
1 2 3 4 5 6 7 |
public class Game { public int Score() { return 0; } } |
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.
Let’s make it pass now:
1 2 3 4 |
public int Score() { return 100; } |
If we run the test now, it will pass.
Congrats! Let’s move on to 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:
1 2 3 4 5 6 |
[Fact] public void Returns50WhenSingleFiveIsRolled() { var game = new Game(); Assert.Equal(50, game.Score()); } |
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.
It looks like we’ll need to fix our Score method to accept a parameter and return the proper score:
1 2 3 4 5 6 |
public int Score(int dieValue) { if (dieValue == 1) return 100; if (dieValue == 5) return 50; return 0; } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
[Fact] public void Returns100WhenSingleOneIsRolled() { var game = new Game(); Assert.Equal(100, game.Score(1)); } [Fact] public void Returns50WhenSingleFiveIsRolled() { var game = new Game(); Assert.Equal(50, game.Score(5)); } |
Both tests should now pass! On to step 3.
Continuing along, let’s write a test to score triple ones:
1 2 3 4 5 6 |
[Fact] public void Returns1000WhenTripleOnesRolled() { var game = new Game(); Assert.Equal(1000, game.ScoreTripleOnes()); } |
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):
1 2 3 4 |
public int ScoreTripleOnes() { return 0; } |
And to fix it:
1 2 3 4 |
public int ScoreTripleOnes() { return 1000; } |
Simple enough (for now)… Let’s try testing the score if 4 ones are rolled.
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:
1 2 3 4 5 6 7 |
[Fact] public void Returns1100WhenOneOneOneOneRolled() { var game = new Game(); int[] dieValues = { 1, 1, 1, 1 }; Assert.Equal(1100, game.CalculateScore(dieValues)); } |
And the CalculateScore() method:
1 2 3 4 5 |
public int CalculateScore(params int[] dieValues) { var score = 0; return score; } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
public int CalculateScore(params int[] dieValues) { var dieCount = new Dictionary<int, int>(); var score = 0; dieCount.Add(1, 0); dieCount.Add(2, 0); dieCount.Add(3, 0); dieCount.Add(4, 0); dieCount.Add(5, 0); dieCount.Add(6, 0); foreach (int i in dieValues) { dieCount[i] += 1; } if (dieCount[1] >= 3) { score += 1000; dieCount[1] -= 3; } score += dieCount[1] * 100; score += dieCount[5] * 50; return score; } |
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.
Let’s refactor the CalculateScore() method to make it easier to read and understand:
1 2 3 4 5 6 7 8 |
public int CalculateScore(params int[] dieValues) { var score = 0; PopulateDieCounts(dieValues); score += ScoreTripleOnes(); score += ScoreSingleDie(); return score; } |
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).
1 2 3 4 5 6 7 8 9 |
private Dictionary<int, int> _dieCounts = new Dictionary<int, int>(); private void PopulateDieCounts(int[] dieValues) { for (int i = 1; i <= 6; i++) { _dieCounts.Add(i, dieValues.Count(d => d == i)); } } |
Then the ScoreTripleOnes() method:
1 2 3 4 5 6 7 8 9 |
public int ScoreTripleOnes() { if (_dieCounts[1] >= 3) { _dieCounts[1] -= 3; return 1000; } return 0; } |
And finally, the ScoreSingleDie() method:
1 2 3 4 5 6 7 |
public int ScoreSingleDie() { var dieValue = 0; dieValue += _dieCounts[1] * 100; dieValue += _dieCounts[5] * 50; return dieValue; } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
public class CalculateScore { [Fact] public void Returns100WhenSingleOneIsRolled() { var game = new Game(); int[] dieValues = { 1 }; Assert.Equal(100, game.CalculateScore(dieValues)); } [Fact] public void Returns50WhenSingleFiveIsRolled() { var game = new Game(); int[] dieValues = { 5 }; Assert.Equal(50, game.CalculateScore(dieValues)); } [Fact] public void Returns1000WhenTripleOnesRolled() { var game = new Game(); int[] dieValues = { 1, 1, 1 }; Assert.Equal(1000, game.CalculateScore(dieValues)); } [Fact] public void Returns1100WhenOneOneOneOneRolled() { var game = new Game(); int[] dieValues = { 1, 1, 1, 1 }; Assert.Equal(1100, game.CalculateScore(dieValues)); } } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
public class Game { private Dictionary<int, int> _dieCounts = new Dictionary<int, int>(); public int ScoreSingleDie() { var dieValue = 0; dieValue += _dieCounts[1] * 100; dieValue += _dieCounts[5] * 50; return dieValue; } public int ScoreTripleOnes() { if (_dieCounts[1] >= 3) { _dieCounts[1] -= 3; return 1000; } return 0; } private void PopulateDieCounts(int[] dieValues) { for (int i = 1; i <= 6; i++) { _dieCounts.Add(i, dieValues.Count(d => d == i)); } } public int CalculateScore(params int[] dieValues) { var score = 0; PopulateDieCounts(dieValues); score += ScoreTripleOnes(); score += ScoreSingleDie(); return score; } } |
Run your tests now and they should all pass.
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:
1 2 3 4 5 6 7 8 9 10 11 |
[Theory] [InlineData(2, 200)] [InlineData(3, 300)] [InlineData(4, 400)] [InlineData(5, 500)] [InlineData(6, 600)] public void ReturnsExpectedValueWhenTripleIsRolled(int dieValue, int expectedScore) { var game = new Game(); Assert.Equal(expectedScore, game.CalculateScore(dieValue, dieValue, dieValue)); } |
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.
There currently isn’t a method to calculate values of triple others. Let’s add one:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
public int ScoreTripleOthers() { if (_dieCounts[2] == 3) { _dieCounts[2] -= 3; return 200; } else if (_dieCounts[3] == 3) { _dieCounts[3] -= 3; return 300; } else if (_dieCounts[4] == 3) { _dieCounts[4] -= 3; return 400; } else if (_dieCounts[5] == 3) { _dieCounts[5] -= 3; return 500; } else if (_dieCounts[6] == 3) { _dieCounts[6] -= 3; return 600; } return 0; } |
We’ll also have to update the CalculateScore() method to add ScoreTripleOthers to the total score:
1 2 3 4 5 6 7 8 9 |
public int CalculateScore(params int[] dieValues) { var score = 0; PopulateDieCounts(dieValues); score += ScoreTripleOnes(); score += ScoreTripleOthers(); score += ScoreSingleDie(); return 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:
1 2 3 4 5 6 7 8 9 10 11 12 |
public int ScoreTripleOthers() { for (int i = 2; i <= 6; i++) { if (_dieCounts[i] == 3) { _dieCounts[i] -= 3; return i * 100; } } return 0; } |
Run the tests again, and they should still pass. Success!
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:
1 2 3 4 5 6 7 |
[Fact] public void Returns1150GivenOneOneOneFiveOne() { var game = new Game(); int[] dieValues = { 1, 1, 1, 5, 1 }; Assert.Equal(1150, game.CalculateScore(dieValues)); } |
Running the test appears to pass without having to change any code. Excellent! Let’s make a few more scenarios:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
[Fact] public void Returns0GivenGarbage() { var game = new Game(); int[] dieValues = { 2, 3, 4, 6, 2 }; Assert.Equal(0, game.CalculateScore(dieValues)); } [Fact] public void Returns350GivenThreeFourFiveThreeThree() { var game = new Game(); int[] dieValues = { 3, 4, 5, 3, 3 }; Assert.Equal(350, game.CalculateScore(dieValues)); } [Fact] public void Returns600GivenAllFives() { var game = new Game(); int[] dieValues = { 5, 5, 5, 5, 5 }; Assert.Equal(600, game.CalculateScore(dieValues)); } |
Oh no, one of the tests failed!
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:
1 2 3 4 5 6 7 8 9 10 11 12 |
public int ScoreTripleOthers() { for (int i = 2; i <= 6; i++) { if (_dieCounts[i] >= 3) { _dieCounts[i] -= 3; return i * 100; } } return 0; } |
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.
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:
1 |
Game _game = new Game(); |
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.
Your final code should look similar to this (download it here):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
public class CalculateScore { Game _game = new Game(); [Fact] public void Returns100WhenSingleOneIsRolled() { int[] dieValues = { 1 }; Assert.Equal(100, _game.CalculateScore(dieValues)); } [Fact] public void Returns50WhenSingleFiveIsRolled() { int[] dieValues = { 5 }; Assert.Equal(50, _game.CalculateScore(dieValues)); } [Fact] public void Returns1000WhenTripleOnesRolled() { int[] dieValues = { 1, 1, 1 }; Assert.Equal(1000, _game.CalculateScore(dieValues)); } [Fact] public void Returns1100WhenOneOneOneOneRolled() { int[] dieValues = { 1, 1, 1, 1 }; Assert.Equal(1100, _game.CalculateScore(dieValues)); } [Theory] [InlineData(2, 200)] [InlineData(3, 300)] [InlineData(4, 400)] [InlineData(5, 500)] [InlineData(6, 600)] public void ReturnsExpectedValueWhenTripleIsRolled(int dieValue, int expectedScore) { Assert.Equal(expectedScore, _game.CalculateScore(dieValue, dieValue, dieValue)); } [Fact] public void Returns1150GivenOneOneOneFiveOne() { int[] dieValues = { 1, 1, 1, 5, 1 }; Assert.Equal(1150, _game.CalculateScore(dieValues)); } [Fact] public void Returns0GivenGarbage() { int[] dieValues = { 2, 3, 4, 6, 2 }; Assert.Equal(0, _game.CalculateScore(dieValues)); } [Fact] public void Returns350GivenThreeFourFiveThreeThree() { int[] dieValues = { 3, 4, 5, 3, 3 }; Assert.Equal(350, _game.CalculateScore(dieValues)); } [Fact] public void Returns600GivenAllFives() { int[] dieValues = { 5, 5, 5, 5, 5 }; Assert.Equal(600, _game.CalculateScore(dieValues)); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
public class Game { private Dictionary<int, int> _dieCounts = new Dictionary<int, int>(); public int ScoreSingleDie() { var dieValue = 0; dieValue += _dieCounts[1] * 100; dieValue += _dieCounts[5] * 50; return dieValue; } public int ScoreTripleOnes() { if (_dieCounts[1] >= 3) { _dieCounts[1] -= 3; return 1000; } return 0; } public int ScoreTripleOthers() { for (int i = 2; i <= 6; i++) { if (_dieCounts[i] >= 3) { _dieCounts[i] -= 3; return i * 100; } } return 0; } private void PopulateDieCounts(int[] dieValues) { for (int i = 1; i <= 6; i++) { _dieCounts.Add(i, dieValues.Count(d => d == i)); } } public int CalculateScore(params int[] dieValues) { var score = 0; PopulateDieCounts(dieValues); score += ScoreTripleOnes(); score += ScoreTripleOthers(); score += ScoreSingleDie(); return score; } } |
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.