- Published on
Taming Time in Kotlin
- Authors
- Name
- Yair Mark
- @yairmark
Most of the time to be able to do something useful in code you need to say get the current date or date and time. This is something that mutates and is hard to test unless you carefully plan it and can lead to "Cinderella bugs" where the code can fail seemingly randomly at very specific times. With Kotlin there is luckily a fairly easy way to tame this and also make it much much easier to test. The other benefit to this approach is you can adjust the clock on code if you ever need to.
A typical method may look like this:
fun doAlertOnExpiry(): Alert {
if(LocalDateTime.now() < someCutOffDate) {
val updatedAlert = alertRecord.copy(updated = LocalDateTime.now(), mustAlert=true)
return alertRepository.save(updatedAlert)
} else {
val updatedAlert = alertRecord.copy(updated = LocalDateTime.now(), mustAlert=false)
return alertRepository.save(updatedAlert)
}
}
This code is fairly simple and contrived but illustrates a few things:
- The tests will be a mission as we cannot accurately test the time values being used
- This code is simple but for more complex code it can break easily due to time being out of our control and is harder to debug/fix due to 1
Luckily in Kotlin and other languages that support telescoping functions, there is an easy solution:
fun doAlertOnExpiry(now: LocalDateTime = LocalDateTime.now(), updatedDatetimeProvider: () -> LocalDateTime = { LocalDateTime.now() }): Alert {
if(now < someCutOffDate) {
val updatedAlert = alertRecord.copy(updated = updatedAtProvider() , mustAlert=true)
return alertRepository.save(updatedAlert)
} else {
val updatedAlert = alertRecord.copy(updated = updatedAtProvider(), mustAlert=false)
return alertRepository.save(updatedAlert)
}
}
We have addressed the issues above it now allows us to:
- Easily test:
- In the test we can tell the test what value to use for
now
using for example:LocalDateTime.of(2022, Month.JUNE, 3, 11, 14,0,0,0)
- In the test we can easily control
updated
by just giving it whatever provider we wanted and even giving it a closure that can adjust the value returned based on the invocation number
- In the test we can tell the test what value to use for
- Easily control the value used for the current time
now
will always be the same when called in the function even when it telescopes i.e. it will be the exact same to the nano second (the old approach would have had differences there where it would change fromLocalDateTime.now()
first being called to when it is called again later)updated
will change but if you wanted to you can easily give the function your own provider to force it to be the sameupdated
value for example{ LocalDateTime.of(2022, Month.JANUARY, 4, 12, 37, 1, 2, 0) }
An example simple closure to illustrate this is as follows:
fun timeClosureExample(): () -> LocalDateTime {
var timesCalled: Int = 0
return {
if(timesCalled == 0){
timesCalled++
LocalDateTime.of(2021, Month.MAY, 4, 14, 54, 1, 3, 0)
} else {
timesCalled++
LocalDateTime.of(2022, Month.DECEMBER, 7, 18, 32, 5, 7, 0)
}
}
}
val callback = timeClosureExample()
println(callback())
println(callback())
println(callback())
This outputs:
2021-05-04T14:54:01:03:00.000Z
2022-12-07T18:32:05:07:00.000Z
2022-12-07T18:32:05:07:00.000Z