TDD LeapYear Kata in Kotlin

This is my attempt at solving a Leap Year kata, where I practice TDD and refactoring techniques like removing duplication from tests and production code, extracting behaviour etc.

I'm currently working through Agile Technical Practices Distilled which has lots of practical tips and techniques that you can practice with the katas at the end of each chapter.

Leap Year

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.

Behaviour 1: Divisible by 4

The first thing I decided to test was whether a year is divisibile by 4. I thought it would be a good place to start, for no major reason.

To pass the first test, I just returned a hard-coded true Boolean value (Fake implementation strategy).

I commit every time a new test passes.

I then added another year that is divisible by 4 to force me to write a slightly more generic solution:

To pass this test, I used an if/else statement because my test doesn't force me to do better than that yet.

I wrote another assertion, this time using 2004 as an example. I expect true to be returned as it is divisible by 4.

I added an or clause to my if statement to make the test pass. So if year is 2000 or 2004, return true, otherwise return false.

I commit this.

There are now three instances of duplication in my production code, so I refactor it to return true if the year is actually divisible by 4 using the modulo operator. It feels uncomfortable writing what feels like hacky code to start with, but removing duplication only when necessary makes it more likely that you are distilling your code down to a single concept, instead of accidentally merging two different concepts.

I'm confident that the behaviour divisible by 4 has been tested fully for the purposes of this kata.

Before moving on, I quickly refactored my tests to remove duplication too.

Behaviour 2: Divisible by 400

The two remaining behaviours are:

• Not a leap year if divisible by 100 EXCEPT
• Is a leap year if divisible by 400

I chose the divisible by 400 behaviour because it doesn't depend on any other behaviours that might be an exception to it's rule, unlike the divisible by 100 rule.

Ah, when writing the test assertion, I realised that all years that are divisible by 400 will also be divisible by 4, so this behaviour is actually only matters because of the divisible by 100 rule. Let's try again.

Behaviour 2 (actually): Divisible by 100

I wrote an assertion to test for a non-leap year that is not divisible by 100.

I wrote the code to pass this test, but it broke the first test. In the first test, 2000 is divisible by 100, but it is a leap year because it is also divisible by 400. So it comes back as a non-leap year because it's divisible by 100 when it should come back as a leap year.

I'm struggling with how to test each of these behaviours seperately if they are all so closely intertwined. Or are they? Not sure.

I ended up changing the 2000 value in the first test to 2020, so that it would be divisible by 4 and not 100. Maybe that's how i'm supposed to handle it, use the values for that specific scenario. It's hard to predict what those are when you have an idea of the other behaviours, so can imagine it being even more difficult with unknown behaviours in a bigger application.

The code that passes the tests:

I immediately don't like this, at all. Too many things happening. Both of the if statements represent different paths the code can go down (called pivot points in the book). I want only one path per method, so will refactor to extract the behaviour into seperate methods.

After extracting the behavior, there are three methods. One checks if a number is divisible by 4, one checks if a number is divisible by 100, and the other ties them togather to return whether the year is a leap year or not based on only those two rules.

I committed the code, removed duplication from the tests and committed again.

Divisible by 400

Here is my first assertion for this behaviour:

To pass the test, I changed the checkIfLeapYear method to the following:

I really like that this code reads like the original problem. Ifdivisible by 4 AND not divisible by 100, OR if divisible by 100 AND divisible by 4 then it is a leap year (true), otherwise it isn't a leap year (false).

When I asked for feedback, I was shown a better solution however, so changed mine to the following:

I'm going to refactor the test names so they read like the problem statement too.

Full Solution

Production Code

Every time I solve a kata, I ask for feedback to help me get better next time. Here is the feedback I got:

I like the look of those tests better than how mine turned out. Next time, I'll think about how to make them more expressive!