Unit testing a method that depends on the current DateTime: Part 1 of 3
This post is based on a short talk I gave some months ago. Let’s say we have a client with a website hosted in one location in Azure. This client has multiple sub-domains associated with physical locations spread across the US. And there is a requirement that each sub-domain displays the time according to the timezone of its associated outlet.
Image source: http://time-time.net/times/time-zones/usa-canada/usa-time-zone-map.php
Let’s start by creating a new demo solution using the following steps in VS2019:
Create a new class library. I named my solution TimeZoneHelper
. Then add a new xUnit test project to the solution
On the test project, right click and choose ‘Manage NuGet packages’. Install the NSubstitute
and FluentAssertions
packages.
We also need to reference the class library from the test project:
In the TimeZoneHelper
project, add a new class named Outlet.cs
with the following code:
using System;
namespace TimeZoneHelper
{
public class Outlet
{
public string GetLocalDateTime(long id)
{
throw new NotImplementedException();
}
}
}
Note that I want to keep things as simple as possible to follow. In a real-life scenario, I would probably use a property to hold the id of the outlet rather than passing it around and keep the return type as DateTime
with the proper timezone offset. The GetLocalDateTime
method just throws an exception. We need to do this otherwise the solution will not compile. Otherwise, if the solution does not compile, we cannot test it. So let’s write the first test.
using FluentAssertions;
using TimeZoneHelper;
using Xunit;
namespace TimeZoneHelperTest
{
public class OutletTest
{
private readonly Outlet sut;
public OutletTest()
{
sut = new Outlet();
}
[Fact]
public void Outlet_can_show_the_time_according_to_its_local_timezone()
{
var result = sut.GetLocalDateTime(1);
result.Should().Be("4/28/2019 17:00");
}
}
}
In this test, we are initialising the Outlet
class and calling its method to check if we get back the result that we wanted. Two things to note. As a convention, I usually use sut
(system under test) or target
for the variable that is used to hold the class to be tested. Second, note that by using FluentAssertions
the test assertion is a very readable, i.e. result.Should().Be(...)
rather than Assert.Equal(...)
.
As expected, the test fails because we’re just throwing an exception. So let’s change that line of code with this:
public string GetLocalDateTime(long id)
{
return DateTime.UtcNow.ToString("M/d/yyyy HH:mm");
}
Running the test, we see a different result, but it still fails:
The reason now is that I am expecting to get a particular point in time, but my current time is different. So this test will only be successful at exactly one point in the history of the world which is not very useful. Otherwise, I would have to change the expected value every time that I run the test, which is not very useful either. And I’ve seen people do this, but we won’t, so don’t even think about it.
The Outlet
class is reading the current system’s date and time directly from the System
object. Let’s try to uncouple the dependency between the two by introducing another object. Enter DateTimeWrapper.cs
:
using System;
namespace TimeZoneHelper
{
public class DateTimeWrapper
{
public DateTime Now()
{
return DateTime.UtcNow;
}
}
}
The Outlet
class can now be modified to use the DateTimeWrapper
:
public class Outlet
{
private readonly DateTimeWrapper dateTimeWrapper;
public Outlet(DateTimeWrapper dateTimeWrapper)
{
this.dateTimeWrapper = dateTimeWrapper;
}
public string GetLocalDateTime(long id)
{
return dateTimeWrapper.Now().ToString("M/d/yyyy HH:mm");
}
}
In this way, the Outlet
object does not need to be concerned with how the date is fetched and this responsibility is shifted to the DateTimeWrapper
. If we need to change how the date is fetched in the future, perhaps because we found a better way to do it, then we won’t need to touch the Outlet
class, which is good. However, at this point the logic is mostly the same as before, so the test still fails with the same outcome. Note that the test constructor now needs to initialise the DateTimeWrapper
as well:
public OutletTest()
{
sut = new Outlet(new DateTimeWrapper());
}
Now is the time that I would typically start cheating. Let’s modify the Now
method in the DateTimeWrapper
:
public DateTime Now()
{
return new DateTime(2019, 4, 28, 17, 00, 00);
}
With this change, the test passes.
We have done our job and we can all go home to rest. We may also brag about our cool code to anyone we are lucky to meet on our way out of the office.
Did we solve our problem completely? Or will we face an unexpected setback? Find out in part 2.