Age calculation using Kotlin extension function with JUnit 5 Tests
by Henry Brown
Recently, I was given another opportunity to implement an age calculator in a project I was working on. While, I have been fortunate enough to be given plenty of opportunities to implement date calculations over my career, it was the first time I had been given the opportunity to solve this problem using Kotlin.
While, the code to perform the calculation was not really much different in Kotlin than it was in Java, I was struck by how much more readable the end result was in Kotlin thanks to the use of Kotlin’s extension functions.
I was so pleased with the result, I decided to write this blog post to commemorate the occasion. Before I do that, however, if you are the type of person who prefers watching videos to reading, you can head over to my YouTube channel and watch the video for this post in glorious high-definition: https://youtu.be/89xzbMGBSrw
With that out of the way, let’s proceed.
Kotlin’s extension functions provides an ability to extend a class with
new functionality without having to inherit from the class or use design patterns such as Decorator
.
Using this mechanism, we can add the required date calculation directly onto the LocalDate
class:
import java.time.LocalDate
import java.time.temporal.ChronoUnit
fun LocalDate.getAgeInYears(): Int = ChronoUnit.YEARS.between(this, LocalDate.now()).toInt()
Note that, within the calculation we can refer to the instance on which the method was called using this
.
The extreme brevity of this implementation is also helped by Kotlin’s support for
single-expression functions.
With this implementation, we can get the age directly from a LocalDate
instance in a very readable manner:
val dateOfBirth = LocalDate.of(1974, 11, 28)
val ageInYears = dateOfBirth.getAgeInYears()
Testing code that relies on the current date
Having readable code is always appreciated. Having that code be correct is often times appreciated it even more. The best way to know that the code performs as expected is to write some unit tests to exercise that code.
One of the difficulties in testing code relies on the current date (or time) is that it can lead to brittle tests. That is a test that passes one day and then fails on another day without any change to the code or the environment.
As the current implementation stands, it would be very difficult to create a test that is not brittle since the implementation
directly depends on the current date internally (by calling: LocalDate.now()
).
Here is where another Kotlin feature can help us address this issue in a very elegant manner.
Kotlin allows us to specify default arguments for functions. This allows us to add an argument to our function which can be omitted in production code but can be used from our test code to control the creation of the date.
The Java API for LocalDate
provides us a convenient way of doing this using the Clock
class. To make this change,
we can change our implementation as follows:
import java.time.Clock
import java.time.LocalDate
import java.time.temporal.ChronoUnit
fun LocalDate.getAgeInYears(clock: Clock = Clock.systemUTC()): Int =
ChronoUnit.YEARS.between(this, LocalDate.now(clock)).toInt()
Here we have updated our code to take the clock
instance from which a new date will be constructed. If none is provided, the
default argument will simply use the system clock to provide the current date for the calculation. However, in our test code
we can supply a different argument for the clock to control how the date is generated.
Speaking of, let’s have a look at the test code. The first thing we need to do, is create a clock that can be used to control the date generation for our age calculation. For that purpose, I like to use the fixed clock which allows me to specify a clock that always returns the exact same instance of time:
const val fixedDate = "2021-03-21"
private val fixedClock = Clock.fixed(Instant.parse("${fixedDate}T00:00:00.00Z"), ZoneOffset.UTC)
Now, I can use this clock to write a test for my age calculation:
@Test
internal fun `should be able to calculate age from a local date`() {
val dateOfBirth = LocalDate.of(2021, 1, 1)
expectThat(dateOfBirth.getAgeInYears(fixedClock)).isEqualTo(0)
}
A simple test, that demonstrates that if the current date was 2021-03-21 and a date of birth of 2021-01-01 then the current age would be zero. We can also be sure, that since we used a fixed clock which always returns the same date, this test will not simply break if run a year from now.
Of course, we probably want to test a couple more interesting situations (e.g. the day before a birthday and the day after a birthday).
The structure of all these tests are going to be the same and we only want to vary the data we want to provide the test.
This seems like a perfect situation to replace our single @Test
with a
@ParameterizedTest
from JUnit 5.
When using @ParameterizedTest
I like to use a Kotlin data class as the argument
to my test. If I override the toString
method in the data class, I find I get very nice output for my tests.
So let’s start by defining the data class to be used:
data class DateOfBirthAgePair(val dateOfBirth: LocalDate, val ageInYears: Int) {
override fun toString() = "$dateOfBirth is $ageInYears years"
}
It contains the date of birth to be used as the test and the expected age in years. I can now update my test to use this data class:
@ParameterizedTest(name = "[{index}] {0} on $fixedDate")
@MethodSource("localDateToAge")
internal fun `should be able to calculate age from a local date`(p: DateOfBirthAgePair) {
val (dateOfBirth: LocalDate, ageInYears: Int) = p
expectThat(dateOfBirth.getAgeInYears(fixedClock)).isEqualTo(ageInYears)
}
The only thing left to do to complete this test, is to define the test data to be used. In this case, I use a @MethodSource
to supply the test data:
@Suppress("unused") //used as MethodSource for [`should be able to calculate age from a local date`]
private fun localDateToAge(): List<DateOfBirthAgePair> = listOf(
DateOfBirthAgePair(dob(2021, 1, 1), 0),
DateOfBirthAgePair(dob(2000, 1, 1), 21),
DateOfBirthAgePair(dob(1974, 3, 20), 47),
DateOfBirthAgePair(dob(1974, 3, 21), 47),
DateOfBirthAgePair(dob(1974, 3, 22), 46),
)
There is one more test, I would like to create before I am happy to call this one done. What if I passed in a date that was in the future? To test this situation, I craft the following test:
@Test
internal fun `should not get a negative age for a date of birth in the future`() {
val tomorrow = LocalDate.now().plusDays(1)
expectThrows<InvalidDateOfBirthException> {
tomorrow.getAgeInYears()
}.and {
get { dob }.isEqualTo(tomorrow)
}
}
In this case, I am not using my fixed clock to set the date. Instead, I am relying on the fact that my test can always construct a date that is in the future (in the example above, by adding a day to the current date). This also demonstrates how the age calculation will be used in non-test code (without specifying a clock instance).
As expected, the test does not pass and requires some changes to be made to make it pass again:
import java.time.Clock
import java.time.LocalDate
import java.time.temporal.ChronoUnit
fun LocalDate.getAgeInYears(clock: Clock = Clock.systemUTC()): Int =
if(this.isAfter(LocalDate.now(clock))) {
throw InvalidDateOfBirthException(this)
} else {
ChronoUnit.YEARS.between(this, LocalDate.now(clock)).toInt()
}
class InvalidDateOfBirthException(val dob: LocalDate):
RuntimeException("Date of birth cannot be in the future. Date of Birth is: $dob")
Conclusion
In this post, we have seen how a number of Kotlin features can lead to very readable and expressive code. Kotlin’s support for extension functions allows us to add functions on to well known types to add more functionality that makes sense in the domain in which we are working.
Moreover, we have seen that writing date calculations can lead to brittle tests if we rely on the current date or time within our
implementation. One approach around this, is to accept a Clock
which allows us to control the creation of the date and time within
the function. Kotlin’s support for default arguments in functions make this extremely easy to implement.
You can download all the code in the proper context from my github repo: https://github.com/hgbrown/kotlin-extensions-age-calculator-demo and don’t forget you can also see all the action on my YouTube channel: https://youtu.be/89xzbMGBSrw
tags: kotlin - testing - strikt - junit