The coding blog of Alastair Smith, a software developer based in Cambridge, UK. Interested in DevOps, Azure, Kubernetes, .NET Core, and VueJS.
In preparation for CAMDUG’s first ever Dojo yesterday, I worked through the Leap Year kata, which I had intended to run in the first session, “Introduction to TDD”. I ended up swapping it out for the Roman Numerals kata, because I got through two iterations of the exercise in about 20 minutes. However, I thought it would be useful to share the problem and my approaches to it with my readers.
The Problem
========
Like many kata, the problem is simply stated:
Write a function that returns true or false depending on whether its input integer is a leap year or not. A leap year is defined as one that is divisible by 4, but is not otherwise divisible by 100 unless it is also divisible by 400.
For example, 2001 is a typical common year and 1996 is a typical leap year, whereas 1900 is an atypical common year and 2000 is an atypical leap year.
For my first iteration, I took an approach that I think is akin to Acceptance Test-Driven Development (ATDD). That is to say, I took the four example years from the problem statement above and used them as my acceptance cases. If all four acceptance tests pass, the system could be considered complete.
[gist:1145099:acceptance_tests.cs]
[gist:1145099:implementation.cs]
I was left a bit uneasy with this approach, however: it seemed too easy, and I was only testing the examples given. How did I know that, for example, 1984 would be considered a leap year and 1985 wouldn’t? What happens if -1984 is provided to the method? What about 0, and other test cases I haven’t even considered yet?
I then deleted my first attempt; this is an important step when doing many iterations of a kata as it helps you to clear your mind of the details of previous solutions and look at the problem anew. I tackled the problem with my more usual form of TDD in the second iteration, which is to say I wrote tests to exercise the method contract rather than fit the provided examples.
My first test exercises the most basic rule of a leap year: if it is divisible by 4, then it is a leap year:
[gist:1145103:FirstTest.cs]
I went with the TDD approach of writing the simplest possible code to make the test pass:
[gist:1145103:FirstImpl.cs]
My second test exercises the inverse condition: if the year is not divisible by 4, then it is not a leap year:
[gist:1145103:SecondTest.cs]
And again, the simplest possible code to get this to pass is:
[gist:1145103:SecondImpl.cs]
I jumped a bit ahead with my third test, and ended up breaking my flow: I wrote a passing test. That’s a bad thing to do, because it means you have to really think hard about whether or not it is passing for the right reason. If they’re passing for the wrong reason and you leave it be, you’re likely to run into problems later.
[gist:1145103:ThirdTest.cs]
Because this test passes, there’s no implementation step.
Instead of writing a test for the situation where the year is divisible by 400, I should have written a test for the situation where the year is divisible by 100, which should return false:
[gist:1145103:FourthTest.cs]
And there is an associated implementation to write for this test:
[gist:1145103:FourthImpl.cs]
Aha! Now test 3 fails. It is hopefully now made clear why I should have written these last two tests the other way around: tests should only start to fail when you break something, which I have not done here. Anyway, I’m now in the situation I would have been in if I had written the third and fourth tests the other way around, and I can write the implementation for the third test:
[gist:1145103:ThirdImpl.cs]
An integral part of the TDD workflow is refactoring: the rhythm that should be adopted is “Red, Green, Refactor”. My chosen refactoring was to pull out the small amount of logic testing whether a year was divisible by 100 into a new method called IsCentury()
:
[gist:1145103:Refactor.cs]
At this point I decided to stop, but there are more refactorings to do (e.g., the code checking whether the year is divisible by 400, and perhaps the one for years divisible by 4 as well). I felt more comfortable with this approach because I was testing the general rules rather than the specific examples. I felt comfortable with the idea of extending this test suite and implementation to handle some of the edge cases (negative numbers, etc) I mentioned above. I think the code for the second iteration is a fair bit cleaner than the first, even though there’s only a couple of small refactorings between them.
I’m interested to read feedback on both these approaches: am I missing something with ATDD? Is my usual TDD approach flawed? Am I refactoring early enough in my workflow?