Take smaller steps with test driven development
- 10 minutes read - 2113 wordsYou can stop improving your debugging skills
When I first started coding in college, I remember how hard programming was in those days. One project, we were tasked to build KABOOM!
Back then, I was not good at design. We’re talking “death from a thousand if’s” here. My professor encouraged everyone to write many comments. Now I understand why… our code was impossible to read! Back then, no ones code read like a book. The night before the due date a few of us from class were working together. I had so many conditionals in my code I couldn’t keep all the logic of the program in my brain. I was not able to compile my program and was completely stuck. I got help from a classmate, and he helped me debug the problem. Back then debugging was a crucial skill to learn. After you get the main process it seems simple. Some people take great pride in their ability to set up breakpoints and track a bunch of variables. However, I was never that good at it. It turns out I don’t need to be a virtuoso debugger. I have TDD.
“I’m not a great programmer, I’m a good programmer with great habits” - Kent Beck
TDD - the great equalizer
Test driven development is an engineering practice that helps me write better code. Not only does writing the tests first allow you to have testable code, but you’ll take smaller steps and be able to iterate on your design.
TDD is about gaining control. The feedback loop that TDD gives you yields so much power. At any moment you can verify your software works the way you expect it to. These automated tests serve as a safety net when we are evolving our software design. Refactoring without tests is very risky. TDD gives developers the confidence they need to refactor at any moment.
But that’s enough theory. The best way to learn TDD is to practice it.
Leap Year TDD Kata
To learn the rhythm and flow of TDD we are going to work on a small exercise together.
Problem Statement
Write a function that returns true or false depending on whether its input integer is a leap year or not.
A leap year is divisible by 4, but is not otherwise divisible by 100 unless it is also divisible by 400.
- 2001 is a typical common year
- 1996 is a typical leap year
- 1992 is another typical leap year
- 1900 is an atypical common year
- 2000 is an atypical leap year
“It takes approximately 365.25 days for Earth to orbit the Sun — a solar year. We usually round the days in a calendar year to 365. To make up for the missing partial day, we add one day to our calendar approximately every four years. That is a leap year.” - NASA
Test setup
If we are going to write tests, we are going to need a testing framework. One of the most popular testing frameworks for Java is JUnit. Today we’ll use the latest and greatest JUnit 5. JUnit has a built-in assertions library, however it is not great at giving useful feedback for failing tests. Instead, we’ll use AssertJ, a fluent assertion library.
To download external libraries into our Java projects we will be making use of a build tool called Gradle. Here’s how my gradle file looks right now. Pay special attention to the dependencies and make sure yours match these. Feel free to use Maven if you prefer. Check out any open source JVM libraries at Maven Central.
Let’s get started by writing a failing test. I like to start new projects like this to make sure everything is set up correctly.
Here we would expect a red bar since true does not equal false.
Now let’s make it pass!
Great! Our testing framework is operating correctly.
Now that our code is in the green we can refactor a bit before we start with the kata.
Let’s begin by statically importing the assertThat method from assertJ. This will reduce the clutter and make our intentions clearer.
Place your cursor on the word assertThat and click option + enter (or alt+enter on WINDOWS). You can also right-click and select Show Context Actions. IntelliJ will give you options on how to can better organize the code.
Select the option saying Add static import
Now we can get rid of unused imports by selecting optimize imports
Here’s the result of that small move
Let’s start by renaming the test method to make it clear about what behavior is under test
Let’s write our first test case!
A unit test is composed of 3 main parts. Arrange, Act, and Assert. We will be focusing on the Act and Assert for the most part since our simple method does not require much setup.
The Act part consists of calling the system under test. The test plays the part of our method’s first caller. When we call the method we want to capture any return variable and compare that with our expectation.
That brings us to the Assert part. This is the true heart of a test case.
This is where we answer the question:
Is what I got back from this method what I expected to get back?
We don’t need to write tests in any particular order, so we are going to start backwards and start with the end in mind.
So for a common year case we are expected the boolean that comes back our method to be false.
Since we have called on a variable that does not exist let’s bring that variable into existence with power of context actions!
Let’s assign the isLeapYear boolean to the return value from a new method that does not exist yet. We will pass in the year 2001 into the new method as it is not a leap year.
Now we can create the class that will contain the method under test. Underneath the test fixture declare a new class called Year. Now we can use context actions again to auto-generate the method signature for isLeapYear.
Instead of returning false like IntelliJ will default to, we should make our test fail first. It’s important to see a failing test before making the test pass. This habit will ensure that the tests we write are useful and will catch regressions.
Now let’s run the tests by clicking the green play button to the left of our test class.
The test fails as expected! Get in the habit of reading the expected and actual values. Understand why the test failed.
At this point, we want to get back to green as quick as possible. So we ask ourselves, “What is the simplest thing we can do to make the test pass?”
Yes! Just return false. That’s the simplest thing!
Now we are back in the green, so we can safely change the structure of the code. Remember to always run your tests after each refactoring move. Our only move this round will be to address the yellow context action hint asking us to use a different assertion method.
This is how the code looks so far
There’s not much else to refactor at this point, so we will add another test case. This time we will pass in a year that is a leap year.
As expected the new test fails, because our method is currently returning false for everything.
Let’s do the simplest thing again and get back to the safety of green. A simple conditional check on the year is the simplest thing to do right now.
Let’s add another leap year case to force us to use a more general algorithm instead of hard coding the year values.
We are failing momentarily. But we can add to our conditional to get back in the green. If year equals 1996 or 1992 then return true.
After the test passes, we can replace the hardcoded values with a proper algorithm. This algorithm follows the original business use case that states that “a leap year is divisible by 4”.
Our tests are starting to grow quite a bit and have tons of duplication. Although duplication in test code is not as sinful as it is in production code. We want to have tests that are easy to read, understand, and maintain. A great way to reduce this kind of duplication is with a parameterized test.
Fully replace existing test cases with parameterized tests.
Let’s run our new parameterized test. We can clearly see the input and expectations listed. This is much more readable.
We can now remove the unneeded test cases as they are covered with the parameterized test now.
Time to write another test case. This time we check the atypical common year case. This is not a leap year, because it is divisible by 100.
As expected, the test fails. Let’s handle this new requirement.
We can make the test pass by simply adding another check in our conditional statement. A leap year is divisible by 4, but is not otherwise divisible by 100.
Refactor time again! Let’s change the parameterized test name to be more accurate.
Let’s add our last test case. The year 2000 is a leap year because it is divisible by 4 and 400.
This test fails as we are currently not handling this requirement.
Our code now handles all the cases. A leap year is divisible by 4, but is not otherwise divisible by 100 unless it is also divisible by 400.
We could definitely stop here. However, since we have our code fully covered by tests we can change the design to improve the readability and future maintainability of the code.
Currently, our code is good, but it could be more readable. What if a junior developer joins our team next week, and they don’t understand what the modulus operator does. It’s not very clear from reading the code that this does what our business use cases originally stated.
One way to improve code readability is to look for duplication. There is some duplication in checking if a number is divisible by another number. Perhaps, we can extract a method out of this.
First, to allow the IDE to work its magic we will prepare for extracting the method. We want our new method to take in a number that we will check if the year is divisible by that number. To do that, we can introduce variables for 4 and 400.
Select the statement year % four == 0
and choose the extract method option from the refactoring menu. Choose a name for the new method like isDivisibleBy.
Next, we can convert isLeapYear to instance method. By doing this we can have year be an instance variable. That way we can have access to it in the isDivisibleBy method. It will make it very easy to read. I’d imagine that Year would hold other properties that are relevant to a year.
Introduce field and default constructor to safely migrate the year currently being passed in through the instance method to be passed in the constructor instead.
Now pass in the givenYear through the new constructor. Remove the this.year = year line.
Safe delete the default constructor as it’s no longer being used.
Oops! Had to reintroduce the default constructor and then convert isDivisibleBy to an instance method. You can do this manually or by selecting convert to instance method on the refactoring menu while having your cursor on the method name. Now it’s safe to remove the default constructor.
Use instance variable instead of parameter.
Now, let’s do the same thing for the code checking that the year is not divisible by 100. We can a extract method called isNotDivisibleBy.
Rename parameters of private methods. Move private methods to bottom of file.
Remove parameter year from isLeapYear and instead use the instance variable that the private methods make use of.
Simplify if else. isLeapYear is now very readable and is at a single level of abstraction.
We’re just about done. We can move the Year class to the main sourceset.
And here’s the test code by itself.
Practice!
TDD is not only extremely effective for programming the software you want to build, but it’s also fun! The continuous cycle of red-green-refactor is very rewarding. TDD gameifies development for me.
I was fortunate to start my career using TDD. I no longer have to experience the pain of trying to recover my broken software. While doing TDD, my code is always 1 minute off of working. This kind of assurance is very liberating.
Here’s a great website I use to find coding katas