Sometimes you use DateTime.Now for more than just setting a timestamp. It could, for example, be to get a time span to see if a certain time has passed since the last time something was executed. And when you do how do you test it?
I admit that I haven't thought about testing DateTime.Now that much until recently since none of the projects I've been working on have used it for much more than timestamps. But here I needed it to make certain a certain time had passed and executed something and set a new runtime. Faced with this, I remembered there is an example of being able to test DateTime.Now in the excellent book The Art of Unit Testing (TAUT) that I will do my version of here.
The base
Let us start with a base. A class with a method for getting a car brand with information about models etc. The car brands are so heavy on the server to get so we cache them for an hour before refreshing the cache.
public class CarBrandsCache
{
private List<CarBrand> _carBrands;
private DateTime _lastRefresh;
private int _cacheTimeInMinutes;
private readonly ICarBrandServer _carBrandServer;
public CarBrandsCache(
ICarBrandSettings carBrandSettings,
ICarBrandServer carBrandServer)
{
_cacheTimeInMinutes = carBrandSettings.CacheTimeInMinutes;
_carBrandServer = carBrandServer;
}
public CarBrand GetCarBrand(string carBrandName)
{
if (DateTime.Now > _lastRefresh.AddMinutes(_cacheTimeInMinutes))
{
_carBrands = _carBrandServer.GetCarBrandsFromServer();
_lastRefresh = DateTime.Now;
}
return _carBrands.Single(c => c.Name == carBrandName);
}
}
Testing this to see that it refreshes the cache is not that clear. We would probably do something like this and it would take a minute to run.
public class CarBrandsCacheTests
{
[Test]
public void GetCarBrand_CacheTimesOutBetweenTwoRequests_ShouldCallServerTwice()
{
var carBrandServer = Substitute.For<ICarBrandServer>();
var carBrandSettings = new CarBrandSettings { CacheTimeInMinutes = 1 };
carBrandServer.GetCarBrandsFromServer().Returns(
new List<CarBrand>
{
new CarBrand { Name = "Volvo" }
});
var carBrandsCache = new CarBrandsCache(carBrandSettings, carBrandServer);
// First call
carBrandsCache.GetCarBrand("Volvo");
// Pause and wait for a minute so we trigger cache
Task.Delay(601000).Wait();
// Second call where cache should refresh
carBrandsCache.GetCarBrand("Volvo");
carBrandServer.Received(2).GetCarBrandsFromServer();
}
}
I guess this works. Saw something similar today, but there is room for improvement. Like not having to wait one minute every run.
ApplicationTime (SystemTime)
Enter what I call ApplicationTime. Initially, I called it SystemTime as in TAUT but after a comment from a colleague where he first thought it had something to do with the actual servers time, so I changed it.
This is the ApplicationTime class.
public static class ApplicationTime
{
private static DateTime? _dateTime;
public static void Set(DateTime customDateTime) => _dateTime = customDateTime;
public static void Reset() => _dateTime = null;
public static DateTime Now() => _dateTime ?? DateTime.Now;
}
The usage in your application is to use ApplicationTime.Now() instead of DateTime.Now. The other methods are for testing purposes where you use Set for setting the DateTime to be used and Reset to return it to normal. After updating the CarBrandsCache class with ApplicationTime it looks like this.
public class CarBrandsCache
{
private List<CarBrand> _carBrands;
private DateTime _lastRefresh;
private int _cacheTimeInMinutes;
private readonly ICarBrandServer _carBrandServer;
public CarBrandsCache(
ICarBrandSettings carBrandSettings,
ICarBrandServer carBrandServer)
{
_cacheTimeInMinutes = carBrandSettings.CacheTimeInMinutes;
_carBrandServer = carBrandServer;
}
public CarBrand GetCarBrand(string carBrandName)
{
if (ApplicationTime.Now() > _lastRefresh.AddMinutes(_cacheTimeInMinutes))
{
_carBrands = _carBrandServer.GetCarBrandsFromServer();
_lastRefresh = ApplicationTime.Now();
}
return _carBrands.Single(c => c.Name == carBrandName);
}
}
The result of using ApplicationTime.Now() make no difference during normal execution but makes a huge difference in testing. We now have the power over time in our tests and can rewrite the test to this.
public class CarBrandsCacheTests
{
[Test]
public void GetCarBrand_CacheTimesOutBetweenTwoRequests_ShouldCallServerTwice()
{
// Set the time for when the test start. This will be the time
// returned by ApplicationTime.Now() and will be set as the last refresh
// after the first call to GetCarBrand
ApplicationTime.Set(new DateTime(2020, 2, 5, 10, 0, 0));
var carBrandServer = Substitute.For<ICarBrandServer>();
var carBrandSettings = new CarBrandSettings { CacheTimeInMinutes = 1 };
carBrandServer.GetCarBrandsFromServer().Returns(
new List<CarBrand>
{
new CarBrand { Name = "Volvo" }
});
var carBrandsCache = new CarBrandsCache(carBrandSettings, carBrandServer);
// First call
carBrandsCache.GetCarBrand("Volvo");
// Let's set a new time based on the cache timeout
// so we trigger a refresh
ApplicationTime.Set(
ApplicationTime.Now().AddMinutes(carBrandSettings.CacheTimeInMinutes + 1));
// Second call where cache should refresh
carBrandsCache.GetCarBrand("Volvo");
carBrandServer.Received(2).GetCarBrandsFromServer();
}
}
We now control what the time is at every step and it executes instantly. We could even do test code where it is days between execution instead of minutes like here with the same speed and control.
How do you do tests when using DateTime.Now?
Disclaimier: The code might not compile since it's just written here in MD.
Top comments (1)
We use similar structure, but it implements IDisposable and keeps stack of DateTime inside. So, we are able to easily define a scope and put one that structure into another.
For the sake of simplicity we named it as "Clock".
IDisposable also helps to prevent "leakage" of tuned clocks. It in theory can make different tests fall sometimes, depending on execution order and test framework.