Like all programmers I have a pretty long held grudge against daylight savings time, leap years, time zones, and just generally anything to do with dates and times. The ways in which date complexity have hurt over the years are many and varied. This year’s pain provided a good opportunity to use the excellent unit test support for time in Dart. This post is a tiny bit about date calculations and mostly about how to get control over dates in tests.
What Went Wrong This Year
This is the history and context part of the post. If you just want to know how to control time in unit tests, feel free to skip this section.
Daylight savings time arrived today and with it the failure of a very simple unit test. The test verified that a method properly calculated that a day one day in the future is one day away. If today is March 8th and I ask for daysFromNow
for March 9th I expect to get one. It’s a pretty simple method, but of course it has to account for things like month wrapping so there is code worth testing. The solution ended up being simple enough that I figured it couldn’t fail. Take the year/month/day of now, find the difference with the year/month/day of tomorrow, convert that difference to days, and that’s the answer. In code:
test('DST example', () {
DateTime today = DateTime(2020, 3, 8);
DateTime tomorrow = DateTime(2020, 3, 9);
var diff = tomorrow.difference(today);
int days = diff.inDays;
expect( days, 1 );
});
This test will fail, you can run it to prove it to yourself if you’d like. Can you spot the error? It took me a while, but I am, all too often, not very bright.
Just before I submitted a very embarrassing bug report to the Dart core team claiming they’d blown a simple calculation, I saw it. Printing diff
in hours makes it clear, it is 23 hours, not 24. Since DST starts after midnight on the 8th then there really are only 23 hours on the day of the 8th. (Wait, that hour actually went somewhere, who knew?) Since a day is 24 hours, not 23, the inDays
method is doing a reasonable thing, although certainly not the expected thing given that code.
Fixing it was easy, just make sure to push the tomorrow date forward enough that even with DST the hours during the day from midnight will exceed 24. This test passes:
test('sample', () {
const int maximumDstLoss = 1; // max hours lost to DST
DateTime today = DateTime(2020, 3, 8);
DateTime tomorrow = DateTime(2020, 3, 9, maximumDstLoss );
var diff = tomorrow.difference(today);
int days = diff.inDays;
expect( days, 1 );
});
Use Clock Instead of DateTime
Dart has a very good DateTime
class with a well thought out set of behaviours. The one thing you should not use it for is to get the current time, despite the very convenient DateTime.now()
method that beckons. Instead always use clock.now()
from the clock package. Here’s an extremely simple example of how to get the current DateTime using the clock.
import 'package:clock/clock.dart';
void main() {
DateTime now = clock.now();
}
Reap the Benefits
That one simple change to the code allows unit tests to control time. For the problem I outlined above it would be very convenient to be able to have a unit test that always checked for the DST start. The test I had failed only because I happened to look at it on the day that DST changed over. The next day it would have been fine. With functions that use DateTime.now()
you can’t easily control the time in a unit test. There are hacks, like writing your own time class and injecting it into the method in question, but there’s a better way. You’ve already seen step one, use clock.now()
. Step two is to take advantage of it in a unit test using the FakeAsync
class. Here’s an example:
test('sample', () {
fakeAsync((testAsync)
{
expect( daysFromNow( DateTime( 2020, 3, 9)), 1 );
}, initialTime: DateTime( 2020, 3, 8));
});
}
int daysFromNow( DateTime future ) {
const int maximumDstLoss = 1; // max hours lost to DST
// This line is the magic, because of the initialTime set
// in the unit test, clock.now() returns that time
DateTime now = clock.now();
DateTime today = DateTime( now.year, now.month, now.day );
DateTime tomorrow = DateTime( future.year, future.month,
future.day, maximumDstLoss);
var diff = tomorrow.difference(today);
return diff.inDays;
}
But Wait! There’s More!
The goodness doesn’t stop there with the clock
package. Most Duration based activities in Dart can be controlled through the fake_async package. The ability to unit test Timers is pretty cool, here’s an example.
import 'dart:async';
import 'package:fake_async/fake_async.dart';
import 'package:test/test.dart';
main() {
test('timer elapses when expected', () {
fakeAsync((testAsync)
{
MyTimer myTimer = MyTimer();
myTimer.startTimer();
testAsync.elapse( Duration( hours: 1 ));
expect( myTimer.timerEvents, 1 );
myTimer.dispose();
});
});
}
class MyTimer {
Timer _timer;
int _timerEvents = 0;
int get timerEvents => _timerEvents;
void dispose() {
_timer?.cancel();
}
void startTimer() {
_timer = Timer.periodic( Duration( hours: 1 ), _onTimer );
}
void _onTimer( Timer activeTimer ) {
++_timerEvents;
}
}
There’s way more easy way: use DateTime.utc(…) instead.