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.
buildscript { | |
ext { | |
jUnitVersion= '5.8.2' | |
assertJVersion= '3.23.1' | |
} | |
repositories { | |
mavenCentral() | |
} | |
} | |
plugins { | |
id 'java' | |
} | |
group 'tech.pathtoprogramming' | |
version '1.0-SNAPSHOT' | |
repositories { | |
mavenCentral() | |
} | |
dependencies { | |
testImplementation "org.junit.jupiter:junit-jupiter-api:$jUnitVersion" | |
testImplementation "org.junit.jupiter:junit-jupiter-engine:$jUnitVersion" | |
testImplementation "org.assertj:assertj-core:$assertJVersion" | |
testImplementation "org.junit.jupiter:junit-jupiter-params:$jUnitVersion" | |
} | |
test { | |
useJUnitPlatform() | |
} |
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.
package tech.pathtoprogramming.tdd; | |
import org.assertj.core.api.Assertions; | |
import org.junit.jupiter.api.Test; | |
class AYearShould { | |
@Test | |
void failingTest() { | |
Assertions.assertThat(true).isEqualTo(false); | |
} | |
} |
Here we would expect a red bar since true does not equal false.
Now let’s make it pass!
package tech.pathtoprogramming.tdd; | |
import org.assertj.core.api.Assertions; | |
import org.junit.jupiter.api.Test; | |
class AYearShould { | |
@Test | |
void failingTest() { | |
Assertions.assertThat(true).isEqualTo(true); | |
} | |
} |
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
package tech.pathtoprogramming.tdd; | |
import org.junit.jupiter.api.Test; | |
import static org.assertj.core.api.Assertions.assertThat; | |
class AYearShould { | |
@Test | |
void failingTest() { | |
assertThat(true).isEqualTo(true); | |
} | |
} |
Let’s start by renaming the test method to make it clear about what behavior is under test
package tech.pathtoprogramming.tdd; | |
import org.junit.jupiter.api.Test; | |
import static org.assertj.core.api.Assertions.assertThat; | |
class AYearShould { | |
@Test | |
void determineThatTheYearIsNotALeapYearForCommonCase() { | |
assertThat(true).isEqualTo(true); | |
} | |
} |
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.
package tech.pathtoprogramming.tdd; | |
import org.junit.jupiter.api.Test; | |
import static org.assertj.core.api.Assertions.assertThat; | |
class AYearShould { | |
@Test | |
void determineThatTheYearIsNotALeapYearForCommonCase() { | |
boolean isLeapYear = Year.isLeapYear(2001); | |
assertThat(isLeapYear).isEqualTo(false); | |
} | |
} | |
class Year { | |
public static boolean isLeapYear(int year) { | |
return true; | |
} | |
} |
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?”
package tech.pathtoprogramming.tdd; | |
import org.junit.jupiter.api.Test; | |
import static org.assertj.core.api.Assertions.assertThat; | |
class AYearShould { | |
@Test | |
void determineThatTheYearIsNotALeapYearForCommonCase() { | |
boolean isLeapYear = Year.isLeapYear(2001); | |
assertThat(isLeapYear).isEqualTo(false); | |
} | |
} | |
class Year { | |
public static boolean isLeapYear(int year) { | |
return false; | |
} | |
} |
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
package tech.pathtoprogramming.tdd; | |
import org.junit.jupiter.api.Test; | |
import static org.assertj.core.api.Assertions.assertThat; | |
class AYearShould { | |
@Test | |
void determineThatTheYearIsNotALeapYearForCommonCase() { | |
boolean isLeapYear = Year.isLeapYear(2001); | |
assertThat(isLeapYear).isFalse(); | |
} | |
} | |
class Year { | |
public static boolean isLeapYear(int year) { | |
return false; | |
} | |
} |
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.
package tech.pathtoprogramming.tdd; | |
import org.junit.jupiter.api.Test; | |
import static org.assertj.core.api.Assertions.assertThat; | |
class AYearShould { | |
@Test | |
void determineThatTheYearIsNotALeapYearForCommonCase() { | |
boolean isLeapYear = Year.isLeapYear(2001); | |
assertThat(isLeapYear).isFalse(); | |
} | |
@Test | |
void determineThatTheYearIsAStandardLeapYear() { | |
boolean isLeapYear = Year.isLeapYear(1996); | |
assertThat(isLeapYear).isTrue(); | |
} | |
} | |
class Year { | |
public static boolean isLeapYear(int year) { | |
return false; | |
} | |
} |
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.
package tech.pathtoprogramming.tdd; | |
import org.junit.jupiter.api.Test; | |
import static org.assertj.core.api.Assertions.assertThat; | |
class AYearShould { | |
@Test | |
void determineThatTheYearIsNotALeapYearForCommonCase() { | |
boolean isLeapYear = Year.isLeapYear(2001); | |
assertThat(isLeapYear).isFalse(); | |
} | |
@Test | |
void determineThatTheYearIsAStandardLeapYear() { | |
boolean isLeapYear = Year.isLeapYear(1996); | |
assertThat(isLeapYear).isTrue(); | |
} | |
} | |
class Year { | |
public static boolean isLeapYear(int year) { | |
if (year == 1996) { | |
return true; | |
} | |
return false; | |
} | |
} |
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.
package tech.pathtoprogramming.tdd; | |
import org.junit.jupiter.api.Test; | |
import static org.assertj.core.api.Assertions.assertThat; | |
class AYearShould { | |
@Test | |
void determineThatTheYearIsNotALeapYearForCommonCase() { | |
boolean isLeapYear = Year.isLeapYear(2001); | |
assertThat(isLeapYear).isFalse(); | |
} | |
@Test | |
void determineThatTheYearIsAStandardLeapYear() { | |
boolean isLeapYear = Year.isLeapYear(1996); | |
assertThat(isLeapYear).isTrue(); | |
} | |
@Test | |
void anotherLeapYear() { | |
boolean isLeapYear = Year.isLeapYear(1992); | |
assertThat(isLeapYear).isTrue(); | |
} | |
} | |
class Year { | |
public static boolean isLeapYear(int year) { | |
if (year == 1996 || year == 1992) { | |
return true; | |
} | |
return false; | |
} | |
} |
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”.
package tech.pathtoprogramming.tdd; | |
import org.junit.jupiter.api.Test; | |
import static org.assertj.core.api.Assertions.assertThat; | |
class AYearShould { | |
@Test | |
void determineThatTheYearIsNotALeapYearForCommonCase() { | |
boolean isLeapYear = Year.isLeapYear(2001); | |
assertThat(isLeapYear).isFalse(); | |
} | |
@Test | |
void determineThatTheYearIsAStandardLeapYear() { | |
boolean isLeapYear = Year.isLeapYear(1996); | |
assertThat(isLeapYear).isTrue(); | |
} | |
@Test | |
void anotherLeapYear() { | |
boolean isLeapYear = Year.isLeapYear(1992); | |
assertThat(isLeapYear).isTrue(); | |
} | |
} | |
class Year { | |
public static boolean isLeapYear(int year) { | |
if (year % 4 == 0) { | |
return true; | |
} | |
return false; | |
} | |
} |
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.
package tech.pathtoprogramming.tdd; | |
import org.junit.jupiter.api.Test; | |
import org.junit.jupiter.params.ParameterizedTest; | |
import org.junit.jupiter.params.provider.CsvSource; | |
import static org.assertj.core.api.Assertions.assertThat; | |
class AYearShould { | |
@ParameterizedTest | |
@CsvSource({"2001,false"}) | |
void determineThatTheYearIsNotALeapYearForCommonCase(int givenYear, boolean isExpectedLeapYear) { | |
boolean isLeapYear = Year.isLeapYear(2001); | |
assertThat(isLeapYear).isEqualTo(false); | |
} | |
@Test | |
void determineThatTheYearIsAStandardLeapYear() { | |
boolean isLeapYear = Year.isLeapYear(1996); | |
assertThat(isLeapYear).isTrue(); | |
} | |
@Test | |
void anotherLeapYear() { | |
boolean isLeapYear = Year.isLeapYear(1992); | |
assertThat(isLeapYear).isTrue(); | |
} | |
} | |
class Year { | |
public static boolean isLeapYear(int year) { | |
if (year % 4 == 0) { | |
return true; | |
} | |
return false; | |
} | |
} |
Fully replace existing test cases with parameterized tests.
package tech.pathtoprogramming.tdd; | |
import org.junit.jupiter.api.Test; | |
import org.junit.jupiter.params.ParameterizedTest; | |
import org.junit.jupiter.params.provider.CsvSource; | |
import static org.assertj.core.api.Assertions.assertThat; | |
class AYearShould { | |
@ParameterizedTest | |
@CsvSource({ | |
"2001,false", | |
"1996,true", | |
"1992,true" | |
}) | |
void determineThatTheYearIsNotALeapYearForCommonCase(int givenYear, boolean isExpectedLeapYear) { | |
boolean isLeapYear = Year.isLeapYear(givenYear); | |
assertThat(isLeapYear).isEqualTo(isExpectedLeapYear); | |
} | |
@Test | |
void determineThatTheYearIsAStandardLeapYear() { | |
boolean isLeapYear = Year.isLeapYear(1996); | |
assertThat(isLeapYear).isTrue(); | |
} | |
@Test | |
void anotherLeapYear() { | |
boolean isLeapYear = Year.isLeapYear(1992); | |
assertThat(isLeapYear).isTrue(); | |
} | |
} | |
class Year { | |
public static boolean isLeapYear(int year) { | |
if (year % 4 == 0) { | |
return true; | |
} | |
return false; | |
} | |
} |
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.
package tech.pathtoprogramming.tdd; | |
import org.junit.jupiter.params.ParameterizedTest; | |
import org.junit.jupiter.params.provider.CsvSource; | |
import static org.assertj.core.api.Assertions.assertThat; | |
class AYearShould { | |
@ParameterizedTest | |
@CsvSource({ | |
"2001,false", | |
"1996,true", | |
"1992,true" | |
}) | |
void determineThatTheYearIsNotALeapYearForCommonCase(int givenYear, boolean isExpectedLeapYear) { | |
boolean isLeapYear = Year.isLeapYear(givenYear); | |
assertThat(isLeapYear).isEqualTo(isExpectedLeapYear); | |
} | |
} | |
class Year { | |
public static boolean isLeapYear(int year) { | |
if (year % 4 == 0) { | |
return true; | |
} | |
return false; | |
} | |
} |
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.
package tech.pathtoprogramming.tdd; | |
import org.junit.jupiter.params.ParameterizedTest; | |
import org.junit.jupiter.params.provider.CsvSource; | |
import static org.assertj.core.api.Assertions.assertThat; | |
class AYearShould { | |
@ParameterizedTest | |
@CsvSource({ | |
"2001,false", | |
"1996,true", | |
"1992,true", | |
"1900,false" | |
}) | |
void determineThatTheYearIsNotALeapYearForCommonCase(int givenYear, boolean isExpectedLeapYear) { | |
boolean isLeapYear = Year.isLeapYear(givenYear); | |
assertThat(isLeapYear).isEqualTo(isExpectedLeapYear); | |
} | |
} | |
class Year { | |
public static boolean isLeapYear(int year) { | |
if (year % 4 == 0) { | |
return true; | |
} | |
return false; | |
} | |
} |
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.
package tech.pathtoprogramming.tdd; | |
import org.junit.jupiter.params.ParameterizedTest; | |
import org.junit.jupiter.params.provider.CsvSource; | |
import static org.assertj.core.api.Assertions.assertThat; | |
class AYearShould { | |
@ParameterizedTest | |
@CsvSource({ | |
"2001,false", | |
"1996,true", | |
"1992,true", | |
"1900,false" | |
}) | |
void determineThatTheYearIsNotALeapYearForCommonCase(int givenYear, boolean isExpectedLeapYear) { | |
boolean isLeapYear = Year.isLeapYear(givenYear); | |
assertThat(isLeapYear).isEqualTo(isExpectedLeapYear); | |
} | |
} | |
class Year { | |
public static boolean isLeapYear(int year) { | |
if (year % 4 == 0 && year % 100 != 0) { | |
return true; | |
} | |
return false; | |
} | |
} |
Refactor time again! Let’s change the parameterized test name to be more accurate.
package tech.pathtoprogramming.tdd; | |
import org.junit.jupiter.params.ParameterizedTest; | |
import org.junit.jupiter.params.provider.CsvSource; | |
import static org.assertj.core.api.Assertions.assertThat; | |
class AYearShould { | |
@ParameterizedTest | |
@CsvSource({ | |
"2001,false", | |
"1996,true", | |
"1992,true", | |
"1900,false" | |
}) | |
void determineWhetherAGivenYearIsALeapYearOrNot(int givenYear, boolean isExpectedLeapYear) { | |
boolean isLeapYear = Year.isLeapYear(givenYear); | |
assertThat(isLeapYear).isEqualTo(isExpectedLeapYear); | |
} | |
} | |
class Year { | |
public static boolean isLeapYear(int year) { | |
if (year % 4 == 0 && year % 100 != 0) { | |
return true; | |
} | |
return false; | |
} | |
} |
Let’s add our last test case. The year 2000 is a leap year because it is divisible by 4 and 400.
package tech.pathtoprogramming.tdd; | |
import org.junit.jupiter.params.ParameterizedTest; | |
import org.junit.jupiter.params.provider.CsvSource; | |
import static org.assertj.core.api.Assertions.assertThat; | |
class AYearShould { | |
@ParameterizedTest | |
@CsvSource({ | |
"2001,false", | |
"1996,true", | |
"1992,true", | |
"1900,false", | |
"2000,true" | |
}) | |
void determineWhetherAGivenYearIsALeapYearOrNot(int givenYear, boolean isExpectedLeapYear) { | |
boolean isLeapYear = Year.isLeapYear(givenYear); | |
assertThat(isLeapYear).isEqualTo(isExpectedLeapYear); | |
} | |
} | |
class Year { | |
public static boolean isLeapYear(int year) { | |
if (year % 4 == 0 && year % 100 != 0) { | |
return true; | |
} | |
return false; | |
} | |
} |
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.
package tech.pathtoprogramming.tdd; | |
import org.junit.jupiter.params.ParameterizedTest; | |
import org.junit.jupiter.params.provider.CsvSource; | |
import static org.assertj.core.api.Assertions.assertThat; | |
class AYearShould { | |
@ParameterizedTest | |
@CsvSource({ | |
"2001,false", | |
"1996,true", | |
"1992,true", | |
"1900,false", | |
"2000,true" | |
}) | |
void determineWhetherAGivenYearIsALeapYearOrNot(int givenYear, boolean isExpectedLeapYear) { | |
boolean isLeapYear = Year.isLeapYear(givenYear); | |
assertThat(isLeapYear).isEqualTo(isExpectedLeapYear); | |
} | |
} | |
class Year { | |
public static boolean isLeapYear(int year) { | |
if (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)) { | |
return true; | |
} | |
return false; | |
} | |
} |
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.
package tech.pathtoprogramming.tdd; | |
import org.junit.jupiter.params.ParameterizedTest; | |
import org.junit.jupiter.params.provider.CsvSource; | |
import static org.assertj.core.api.Assertions.assertThat; | |
class AYearShould { | |
@ParameterizedTest | |
@CsvSource({ | |
"2001,false", | |
"1996,true", | |
"1992,true", | |
"1900,false", | |
"2000,true" | |
}) | |
void determineWhetherAGivenYearIsALeapYearOrNot(int givenYear, boolean isExpectedLeapYear) { | |
boolean isLeapYear = Year.isLeapYear(givenYear); | |
assertThat(isLeapYear).isEqualTo(isExpectedLeapYear); | |
} | |
} | |
class Year { | |
public static boolean isLeapYear(int year) { | |
int four = 4; | |
int fourHundred = 400; | |
if (year % four == 0 && (year % 100 != 0 || year % fourHundred == 0)) { | |
return true; | |
} | |
return false; | |
} | |
} |
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.
package tech.pathtoprogramming.tdd; | |
import org.junit.jupiter.params.ParameterizedTest; | |
import org.junit.jupiter.params.provider.CsvSource; | |
import static org.assertj.core.api.Assertions.assertThat; | |
class AYearShould { | |
@ParameterizedTest | |
@CsvSource({ | |
"2001,false", | |
"1996,true", | |
"1992,true", | |
"1900,false", | |
"2000,true" | |
}) | |
void determineWhetherAGivenYearIsALeapYearOrNot(int givenYear, boolean isExpectedLeapYear) { | |
boolean isLeapYear = Year.isLeapYear(givenYear); | |
assertThat(isLeapYear).isEqualTo(isExpectedLeapYear); | |
} | |
} | |
class Year { | |
public static boolean isLeapYear(int year) { | |
if (isDivisibleBy(year, 4) && (year % 100 != 0 || isDivisibleBy(year, 400))) { | |
return true; | |
} | |
return false; | |
} | |
private static boolean isDivisibleBy(int year, int four) { | |
return year % four == 0; | |
} | |
} |
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.
package tech.pathtoprogramming.tdd; | |
import org.junit.jupiter.params.ParameterizedTest; | |
import org.junit.jupiter.params.provider.CsvSource; | |
import static org.assertj.core.api.Assertions.assertThat; | |
class AYearShould { | |
@ParameterizedTest | |
@CsvSource({ | |
"2001,false", | |
"1996,true", | |
"1992,true", | |
"1900,false", | |
"2000,true" | |
}) | |
void determineWhetherAGivenYearIsALeapYearOrNot(int givenYear, boolean isExpectedLeapYear) { | |
boolean isLeapYear = new Year().isLeapYear(givenYear); | |
assertThat(isLeapYear).isEqualTo(isExpectedLeapYear); | |
} | |
} | |
class Year { | |
private static boolean isDivisibleBy(int year, int four) { | |
return year % four == 0; | |
} | |
public boolean isLeapYear(int year) { | |
if (isDivisibleBy(year, 4) && (year % 100 != 0 || isDivisibleBy(year, 400))) { | |
return true; | |
} | |
return false; | |
} | |
} |
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.
package tech.pathtoprogramming.tdd; | |
import org.junit.jupiter.params.ParameterizedTest; | |
import org.junit.jupiter.params.provider.CsvSource; | |
import static org.assertj.core.api.Assertions.assertThat; | |
class AYearShould { | |
@ParameterizedTest | |
@CsvSource({ | |
"2001,false", | |
"1996,true", | |
"1992,true", | |
"1900,false", | |
"2000,true" | |
}) | |
void determineWhetherAGivenYearIsALeapYearOrNot(int givenYear, boolean isExpectedLeapYear) { | |
boolean isLeapYear = new Year().isLeapYear(givenYear); | |
assertThat(isLeapYear).isEqualTo(isExpectedLeapYear); | |
} | |
} | |
class Year { | |
private int year; | |
public Year() { | |
} | |
private static boolean isDivisibleBy(int year, int four) { | |
return year % four == 0; | |
} | |
public boolean isLeapYear(int year) { | |
this.year = year; | |
if (isDivisibleBy(this.year, 4) && (year % 100 != 0 || isDivisibleBy(year, 400))) { | |
return true; | |
} | |
return false; | |
} | |
} |
Now pass in the givenYear through the new constructor. Remove the this.year = year line.
package tech.pathtoprogramming.tdd; | |
import org.junit.jupiter.params.ParameterizedTest; | |
import org.junit.jupiter.params.provider.CsvSource; | |
import static org.assertj.core.api.Assertions.assertThat; | |
class AYearShould { | |
@ParameterizedTest | |
@CsvSource({ | |
"2001,false", | |
"1996,true", | |
"1992,true", | |
"1900,false", | |
"2000,true" | |
}) | |
void determineWhetherAGivenYearIsALeapYearOrNot(int givenYear, boolean isExpectedLeapYear) { | |
boolean isLeapYear = new Year(givenYear).isLeapYear(givenYear); | |
assertThat(isLeapYear).isEqualTo(isExpectedLeapYear); | |
} | |
} | |
class Year { | |
private int year; | |
public Year() { | |
} | |
public Year(int year) { | |
this.year = year; | |
} | |
private static boolean isDivisibleBy(int year, int four) { | |
return year % four == 0; | |
} | |
public boolean isLeapYear(int year) { | |
if (isDivisibleBy(this.year, 4) && (year % 100 != 0 || isDivisibleBy(year, 400))) { | |
return true; | |
} | |
return false; | |
} | |
} |
Safe delete the default constructor as it’s no longer being used.
package tech.pathtoprogramming.tdd; | |
import org.junit.jupiter.params.ParameterizedTest; | |
import org.junit.jupiter.params.provider.CsvSource; | |
import static org.assertj.core.api.Assertions.assertThat; | |
class AYearShould { | |
@ParameterizedTest | |
@CsvSource({ | |
"2001,false", | |
"1996,true", | |
"1992,true", | |
"1900,false", | |
"2000,true" | |
}) | |
void determineWhetherAGivenYearIsALeapYearOrNot(int givenYear, boolean isExpectedLeapYear) { | |
boolean isLeapYear = new Year(givenYear).isLeapYear(givenYear); | |
assertThat(isLeapYear).isEqualTo(isExpectedLeapYear); | |
} | |
} | |
class Year { | |
private int year; | |
public Year(int year) { | |
this.year = year; | |
} | |
private static boolean isDivisibleBy(int year, int four) { | |
return year % four == 0; | |
} | |
public boolean isLeapYear(int year) { | |
if (isDivisibleBy(this.year, 4) && (year % 100 != 0 || isDivisibleBy(year, 400))) { | |
return true; | |
} | |
return false; | |
} | |
} |
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.
package tech.pathtoprogramming.tdd; | |
import org.junit.jupiter.params.ParameterizedTest; | |
import org.junit.jupiter.params.provider.CsvSource; | |
import static org.assertj.core.api.Assertions.assertThat; | |
class AYearShould { | |
@ParameterizedTest | |
@CsvSource({ | |
"2001,false", | |
"1996,true", | |
"1992,true", | |
"1900,false", | |
"2000,true" | |
}) | |
void determineWhetherAGivenYearIsALeapYearOrNot(int givenYear, boolean isExpectedLeapYear) { | |
boolean isLeapYear = new Year(givenYear).isLeapYear(givenYear); | |
assertThat(isLeapYear).isEqualTo(isExpectedLeapYear); | |
} | |
} | |
class Year { | |
private int year; | |
public Year(int year) { | |
this.year = year; | |
} | |
private boolean isDivisibleBy(int year, int four) { | |
return year % four == 0; | |
} | |
public boolean isLeapYear(int year) { | |
if (isDivisibleBy(this.year, 4) && (year % 100 != 0 || isDivisibleBy(year, 400))) { | |
return true; | |
} | |
return false; | |
} | |
} |
Use instance variable instead of parameter.
package tech.pathtoprogramming.tdd; | |
import org.junit.jupiter.params.ParameterizedTest; | |
import org.junit.jupiter.params.provider.CsvSource; | |
import static org.assertj.core.api.Assertions.assertThat; | |
class AYearShould { | |
@ParameterizedTest | |
@CsvSource({ | |
"2001,false", | |
"1996,true", | |
"1992,true", | |
"1900,false", | |
"2000,true" | |
}) | |
void determineWhetherAGivenYearIsALeapYearOrNot(int givenYear, boolean isExpectedLeapYear) { | |
boolean isLeapYear = new Year(givenYear).isLeapYear(givenYear); | |
assertThat(isLeapYear).isEqualTo(isExpectedLeapYear); | |
} | |
} | |
class Year { | |
private int year; | |
public Year(int year) { | |
this.year = year; | |
} | |
private boolean isDivisibleBy(int year, int four) { | |
return this.year % four == 0; | |
} | |
public boolean isLeapYear(int year) { | |
if (isDivisibleBy(this.year, 4) && (year % 100 != 0 || isDivisibleBy(year, 400))) { | |
return true; | |
} | |
return false; | |
} | |
} |
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.
package tech.pathtoprogramming.tdd; | |
import org.junit.jupiter.params.ParameterizedTest; | |
import org.junit.jupiter.params.provider.CsvSource; | |
import static org.assertj.core.api.Assertions.assertThat; | |
class AYearShould { | |
@ParameterizedTest | |
@CsvSource({ | |
"2001,false", | |
"1996,true", | |
"1992,true", | |
"1900,false", | |
"2000,true" | |
}) | |
void determineWhetherAGivenYearIsALeapYearOrNot(int givenYear, boolean isExpectedLeapYear) { | |
boolean isLeapYear = new Year(givenYear).isLeapYear(givenYear); | |
assertThat(isLeapYear).isEqualTo(isExpectedLeapYear); | |
} | |
} | |
class Year { | |
private int year; | |
public Year(int year) { | |
this.year = year; | |
} | |
private boolean isDivisibleBy(int four) { | |
return this.year % four == 0; | |
} | |
public boolean isLeapYear(int year) { | |
if (isDivisibleBy(4) && (isNotDivisibleBy(year, 100) || isDivisibleBy(400))) { | |
return true; | |
} | |
return false; | |
} | |
private boolean isNotDivisibleBy(int year, int hundred) { | |
return year % hundred != 0; | |
} | |
} |
Rename parameters of private methods. Move private methods to bottom of file.
package tech.pathtoprogramming.tdd; | |
import org.junit.jupiter.params.ParameterizedTest; | |
import org.junit.jupiter.params.provider.CsvSource; | |
import static org.assertj.core.api.Assertions.assertThat; | |
class AYearShould { | |
@ParameterizedTest | |
@CsvSource({ | |
"2001,false", | |
"1996,true", | |
"1992,true", | |
"1900,false", | |
"2000,true" | |
}) | |
void determineWhetherAGivenYearIsALeapYearOrNot(int givenYear, boolean isExpectedLeapYear) { | |
boolean isLeapYear = new Year(givenYear).isLeapYear(givenYear); | |
assertThat(isLeapYear).isEqualTo(isExpectedLeapYear); | |
} | |
} | |
class Year { | |
private int year; | |
public Year(int year) { | |
this.year = year; | |
} | |
public boolean isLeapYear(int year) { | |
if (isDivisibleBy(4) && (isNotDivisibleBy(year, 100) || isDivisibleBy(400))) { | |
return true; | |
} | |
return false; | |
} | |
private boolean isDivisibleBy(int number) { | |
return this.year % number == 0; | |
} | |
private boolean isNotDivisibleBy(int year, int number) { | |
return year % number != 0; | |
} | |
} |
Remove parameter year from isLeapYear and instead use the instance variable that the private methods make use of.
package tech.pathtoprogramming.tdd; | |
import org.junit.jupiter.params.ParameterizedTest; | |
import org.junit.jupiter.params.provider.CsvSource; | |
import static org.assertj.core.api.Assertions.assertThat; | |
class AYearShould { | |
@ParameterizedTest | |
@CsvSource({ | |
"2001,false", | |
"1996,true", | |
"1992,true", | |
"1900,false", | |
"2000,true" | |
}) | |
void determineWhetherAGivenYearIsALeapYearOrNot(int givenYear, boolean isExpectedLeapYear) { | |
boolean isLeapYear = new Year(givenYear).isLeapYear(); | |
assertThat(isLeapYear).isEqualTo(isExpectedLeapYear); | |
} | |
} | |
class Year { | |
private int year; | |
public Year(int year) { | |
this.year = year; | |
} | |
public boolean isLeapYear() { | |
if (isDivisibleBy(4) && (isNotDivisibleBy(100) || isDivisibleBy(400))) { | |
return true; | |
} | |
return false; | |
} | |
private boolean isDivisibleBy(int number) { | |
return this.year % number == 0; | |
} | |
private boolean isNotDivisibleBy(int number) { | |
return this.year % number != 0; | |
} | |
} |
Simplify if else. isLeapYear is now very readable and is at a single level of abstraction.
package tech.pathtoprogramming.tdd; | |
import org.junit.jupiter.params.ParameterizedTest; | |
import org.junit.jupiter.params.provider.CsvSource; | |
import static org.assertj.core.api.Assertions.assertThat; | |
class AYearShould { | |
@ParameterizedTest | |
@CsvSource({ | |
"2001,false", | |
"1996,true", | |
"1992,true", | |
"1900,false", | |
"2000,true" | |
}) | |
void determineWhetherAGivenYearIsALeapYearOrNot(int givenYear, boolean isExpectedLeapYear) { | |
boolean isLeapYear = new Year(givenYear).isLeapYear(); | |
assertThat(isLeapYear).isEqualTo(isExpectedLeapYear); | |
} | |
} | |
class Year { | |
private int year; | |
public Year(int year) { | |
this.year = year; | |
} | |
public boolean isLeapYear() { | |
return isDivisibleBy(4) | |
&& (isNotDivisibleBy(100) | |
|| isDivisibleBy(400)); | |
} | |
private boolean isDivisibleBy(int number) { | |
return this.year % number == 0; | |
} | |
private boolean isNotDivisibleBy(int number) { | |
return this.year % number != 0; | |
} | |
} |
We’re just about done. We can move the Year class to the main sourceset.
package tech.pathtoprogramming.tdd; | |
class Year { | |
private int year; | |
public Year(int year) { | |
this.year = year; | |
} | |
public boolean isLeapYear() { | |
return isDivisibleBy(4) | |
&& (isNotDivisibleBy(100) | |
|| isDivisibleBy(400)); | |
} | |
private boolean isDivisibleBy(int number) { | |
return this.year % number == 0; | |
} | |
private boolean isNotDivisibleBy(int number) { | |
return this.year % number != 0; | |
} | |
} |
And here’s the test code by itself.
package tech.pathtoprogramming.tdd; | |
import org.junit.jupiter.params.ParameterizedTest; | |
import org.junit.jupiter.params.provider.CsvSource; | |
import static org.assertj.core.api.Assertions.assertThat; | |
class AYearShould { | |
@ParameterizedTest | |
@CsvSource({ | |
"2001,false", | |
"1996,true", | |
"1992,true", | |
"1900,false", | |
"2000,true" | |
}) | |
void determineWhetherAGivenYearIsALeapYearOrNot(int givenYear, boolean isExpectedLeapYear) { | |
boolean isLeapYear = new Year(givenYear).isLeapYear(); | |
assertThat(isLeapYear).isEqualTo(isExpectedLeapYear); | |
} | |
} |
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