These days I spend a fair amount of time introducing developers to TDD and I have seen how developers new to TDD grapple to get their heads around exactly what it's all about. In the rest of this post I will try to explain how the "test first" process of TDD works using a very simple example. The example is based on the FizzBuzz kata. I am not going to repeat the kata text here, but for anyone reading this article who is not familiar with the kata, I would suggest following this link to the kata and read through the problem description.
Uncle Bob has a few simple rules that you should follow when doing TDD.
- You are not allowed to write any production code unless it is to make a failing unit test pass.
- You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.
- You are not allowed to write any more production code than is sufficient to pass the one failing unit test.
So to get started we are going to need to write a failing test.
using System; using NUnit.Framework; namespace FizzBuzz { [TestFixture] public class TestFizzBuzz { [Test] public void GetFizzBuzz_WhenInputIs_1_ShouldReturn_1() { //---------------Set up test pack------------------- const string expected = "1"; //---------------Assert Precondition---------------- //---------------Execute Test ---------------------- var actual = GetFizzBuzz(1); //---------------Test Result ----------------------- Assert.AreEqual(expected, actual); } private string GetFizzBuzz(int input) { return "0"; } }The name of the test above should be self explanatory. Notice in the GetFizzBuzz method I am simply returning "0" which will cause the first test to fail. To fix the failing test is pretty straight forward.
private string GetFizzBuzz(int input) { return "1"; }I think that most people will agree that the first test is pretty straight forward and to get the test to pass is as easy as changing a single character in the System Under Test (SUT). The System Under Test in this case is the GetFizzBuzz method. In my next test I want to make the smallest change I can to start evolving the algorithm of the GetFizzBuzz method.
[Test] public void GetFizzBuzz_WhenInputIs_2_ShouldReturn_2() { //---------------Set up test pack------------------- const string expected = "2"; //---------------Assert Precondition---------------- //---------------Execute Test ---------------------- var actual = GetFizzBuzz(2); //---------------Test Result ----------------------- Assert.AreEqual(expected, actual); }We once again have a failing test that we need to fix.
private string GetFizzBuzz(int input) { if (input==1) return "1"; return "2"; }Run the tests and they will now both pass. On to test 3.
[Test] public void GetFizzBuzz_WhenInputIs_3_ShouldReturn_Fizz() { //---------------Set up test pack------------------- const string expected = "Fizz"; //---------------Assert Precondition---------------- //---------------Execute Test ---------------------- var actual = GetFizzBuzz(3); //---------------Test Result ----------------------- Assert.AreEqual(expected, actual); }And to fix the failing test.
private string GetFizzBuzz(int input) { if (input==1) return "1"; if (input == 2) return "2"; return "Fizz"; }We are green again. On to the next test.
[Test] public void GetFizzBuzz_WhenInputIs_4_ShouldReturn_4() { //---------------Set up test pack------------------- const string expected = "4"; //---------------Assert Precondition---------------- //---------------Execute Test ---------------------- var actual = GetFizzBuzz(4); //---------------Test Result ----------------------- Assert.AreEqual(expected, actual); }And to make this test pass.
private string GetFizzBuzz(int input) { if (input==1) return "1"; if (input == 2) return "2"; if (input == 4) return "4"; return "Fizz"; }Once again we are green. By this point it should be pretty apparent that a pattern is emerging and we can refactor to remove the code duplication. So lets refactor.
private string GetFizzBuzz(int input) { if (input == 3) return "Fizz"; return Convert.ToString(input); }That's better. The duplication is gone and the code is much more readable. Lets continue.
[Test] public void GetFizzBuzz_WhenInputIs_5_ShouldReturn_Buzz() { //---------------Set up test pack------------------- const string expected = "Buzz"; //---------------Assert Precondition---------------- //---------------Execute Test ---------------------- var actual = GetFizzBuzz(5); //---------------Test Result ----------------------- Assert.AreEqual(expected, actual); }Another failing test. And the fix...
private string GetFizzBuzz(int input) { if (input == 5) return "Buzz"; if (input == 3) return "Fizz"; return Convert.ToString(input); }We're green again. On to 6.
[Test] public void GetFizzBuzz_WhenInputIs_6_ShouldReturn_Fizz() { //---------------Set up test pack------------------- const string expected = "Fizz"; //---------------Assert Precondition---------------- //---------------Execute Test ---------------------- var actual = GetFizzBuzz(6); //---------------Test Result ----------------------- Assert.AreEqual(expected, actual); }Getting this test to pass reveals another pattern starting to emerge.
private string GetFizzBuzz(int input) { if (input == 5) return "Buzz"; if (input == 3 || input == 6) return "Fizz"; return Convert.ToString(input); }Not time to refactor just yet. It's generally accepted that we should apply 'the rule of three' when it comes to refactoring. At this point we aren't going to get any value writing tests for 7 and 8 (they should just pass) so lets move on to a test for 9.
[Test] public void GetFizzBuzz_WhenInputIs_9_ShouldReturn_Fizz() { //---------------Set up test pack------------------- const string expected = "Fizz"; //---------------Assert Precondition---------------- //---------------Execute Test ---------------------- var actual = GetFizzBuzz(9); //---------------Test Result ----------------------- Assert.AreEqual(expected, actual); }
And to get the test to pass.
private string GetFizzBuzz(int input) { if (input == 5) return "Buzz"; if (input == 3 || input == 6 || input == 9) return "Fizz"; return Convert.ToString(input); }Green again. Notice the repeated code (times 3). Time to refactor.
private string GetFizzBuzz(int input) { if (input == 5) return "Buzz"; if (input % 3 == 0) return "Fizz"; return Convert.ToString(input); }Run the tests and we are still green. That's better. At this point it should be pretty obvious the same pattern should emerge for 5. To speed things along I am going to use NUnit's TestCase to write tests for 3 examples that are divisible by 5.
[TestCase(5)] [TestCase(10)] [TestCase(15)] public void GetFizzBuzz_WhenInputIsMultipleOf_5_ShouldReturn_Buzz(int input) { //---------------Set up test pack------------------- const string expected = "Buzz"; //---------------Assert Precondition---------------- //---------------Execute Test ---------------------- var actual = GetFizzBuzz(input); //---------------Test Result ----------------------- Assert.AreEqual(expected, actual); }Notice the test name. I have generalised it a little so that it makes sense for all the test case examples. When using tests cases you need to be aware that there is a balance in terms of how general you allow your tests to become, the test name should help you here. If you make the tests to general, you will lose the intent of the test and down the line other developers looking at your tests will find it much harder to work out what your code is doing. e.g.
[TestCase(1)] [TestCase(2)] [TestCase(3)] [TestCase(4)] [TestCase(5)] public void GetFizzBuzz_GivenANumericInput_ShouldReturnAStringResult(int input) { // THIS IS A BAD TEST. IT IS WAY TO GENERALISED //---------------Set up test pack------------------- const string expected = "Buzz"; //---------------Assert Precondition---------------- //---------------Execute Test ---------------------- var actual = GetFizzBuzz(input); //---------------Test Result ----------------------- Assert.AreEqual(expected, actual); }DON'T WRITE TESTS LIKE THE ONE ABOVE. JUST DON'T!
Ok. Let's get our tests passing.
private string GetFizzBuzz(int input) { if (input % 5 == 0) return "Buzz"; if (input % 3 == 0) return "Fizz"; return Convert.ToString(input); }Moving on. We need to deal with the case where a number is divisible by 3 and 5.
[TestCase(15)] public void GetFizzBuzz_WhenInputIsMultipleOf_3_And_5_ShouldReturn_FizzBuzz(int input) { //---------------Set up test pack------------------- const string expected = "FizzBuzz"; //---------------Assert Precondition---------------- //---------------Execute Test ---------------------- var actual = GetFizzBuzz(input); //---------------Test Result ----------------------- Assert.AreEqual(expected, actual); }And to get the test to pass.
private string GetFizzBuzz(int input) { if (input == 3*5) return "FizzBuzz"; if (input % 5 == 0) return "Buzz"; if (input % 3 == 0) return "Fizz"; return Convert.ToString(input); }My new test passes, but I have a failure. What's gone wrong? Turns out that making the big step with the test cases for 5 I didn't consider numbers that are divisible by 3 and 5. I need to fix the tests case for 15. I'll change it to 20.
[TestCase(5)] [TestCase(10)] [TestCase(20)] public void GetFizzBuzz_WhenInputIsMultipleOf_5_ShouldReturn_Buzz(int input) { //---------------Set up test pack------------------- const string expected = "Buzz"; //---------------Assert Precondition---------------- //---------------Execute Test ---------------------- var actual = GetFizzBuzz(input); //---------------Test Result ----------------------- Assert.AreEqual(expected, actual); }Ok. The tests are all passing again. I'll add the necessary test cases for FizzBuzz (divisible by 3 and 5).
[TestCase(15)] [TestCase(30)] [TestCase(45)] public void GetFizzBuzz_WhenInputIsMultipleOf_3_And_5_ShouldReturn_FizzBuzz(int input) { //---------------Set up test pack------------------- const string expected = "FizzBuzz"; //---------------Assert Precondition---------------- //---------------Execute Test ---------------------- var actual = GetFizzBuzz(input); //---------------Test Result ----------------------- Assert.AreEqual(expected, actual); }And change the code to get all the tests passing.
private string GetFizzBuzz(int input) { if (input % (3*5) == 0) return "FizzBuzz"; if (input % 5 == 0) return "Buzz"; if (input % 3 == 0) return "Fizz"; return Convert.ToString(input); }That's it the GetFizzBuzz method should now work for any integer value greater than 0. I should possibly include a test to ensure that the input is always greater than 0, but I'll leave that up to you. Just to finish the kata off quickly, i'll include one further test to test GetFizzBuzz list that accepts a "count" and returns a list of FizzBuzz results that has "count" items.
[Test] public void GetFizzList_Given_100_ShouldReturn_First100FizzBuzzresults() { //---------------Set up test pack------------------- //---------------Assert Precondition---------------- //---------------Execute Test ---------------------- ListAnd to get the test to pass.results = GetFizzBuzzList(100); //---------------Test Result ----------------------- foreach (var result in results) { Console.WriteLine(result); } Assert.AreEqual(100, results.Count); Assert.AreEqual("1", results[0]); Assert.AreEqual("Fizz", results[2]); Assert.AreEqual("Buzz", results[4]); Assert.AreEqual("FizzBuzz", results[14]); Assert.AreEqual("FizzBuzz", results[89]); Assert.AreEqual("Fizz", results[98]); Assert.AreEqual("Buzz", results[99]); }
private ListGreen again. That's it.GetFizzBuzzList(int count) { var results = new List (); for (int i = 0; i < count; i++) { string result = GetFizzBuzz(i+1); results.Add(result); } return results; }