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

Adding a test project to the solution

On the test project, right click and choose ‘Manage NuGet packages’. Install the NSubstitute and FluentAssertions packages.

Installation of the required nuget packages for testing

We also need to reference the class library from the test project:

Referencing the base 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(...).

Initial failing test

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:

Test still failing

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.

Test is passing!

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.

0 comments

Add your comments