Unit testing a method that depends on the current DateTime: Part 2 of 3

Back in part 1 I jokingly closed with a test passing, but I was cheating a bit by building a method that returns a hardcoded string containing the expected result. This is perfectly acceptable in the initial phase to make a test pass, if only to be sure that the rest of the scaffolding was done correctly.

While driving home that day in the style of Papa Pig, an idea starts forming. What if we write another test to check for a second case? Back in the office the next day we add this test case for the fictional outlet with an id of 2. We need to change the test method into a parametrised test method:

[Theory]
[InlineData(0, "4/28/2019 10:00")]
[InlineData(1, "4/28/2019 12:00")]
public void Outlet_can_show_the_time_according_to_its_local_timezone(
    int outletId, string expectedResult)
{
    var result = sut.GetLocalDateTime(outletId);
    result.Should().Be(expectedResult);
}

In this way we may use the same test for multiple values. If we run the tests only one is passing, so we have a problem once more.

Only one of the two tests is passing

Let’s solve the problem properly this time. What we need is another layer of indirection. The Outlet class was directly dependant on the System.DateTime class, and with our change we only made it dependant on the DateTimeWrapper class. We still cannot control the behaviour of the date within our test. We need to add an interface that specifies the wrapper’s behaviour and then we will be able to mock it during the test execution. Visual Studio has a nice refactoring tool that allows us to quickly generate the interface:

Extracting the DateTimeWrapper Interface

It even allows us to choose the name and which methods to include in the interface:

We also have some control when extracting the interface

To be honest, I'm not sure if this feature is standard in all versions of Visual Studio, but here is the generated code anyway:

using System;

namespace TimeZoneHelper
{
    public interface IDateTimeWrapper
    {
        DateTime Now();
    }
}

That’s it, and the DateTimeWrapper class now also inherits the IDateTimeWrapper interface. Let’s use it! In the Outlet class change the type to IDateTimeWrapper:

public class Outlet
{
    private readonly IDateTimeWrapper dateTimeWrapper;

    public Outlet(IDateTimeWrapper dateTimeWrapper)
    {
        this.dateTimeWrapper = dateTimeWrapper;
    }

    public string GetLocalDateTime(long id)
    {
        return dateTimeWrapper.Now().ToString("M/d/yyyy HH:mm");
    }
}

Now any object that implements IDateTimeWrapper can be used with the Outlet class. Note that the choice for initialising the required wrapper class will be handled by the dependency injection middleware, but we won’t go into it here as we will just focus on the test.

In the test class, we will also use the interface, but on top of that we will mock it as well. It’s not that we want to make fun of it, we just want to be able to control what happens during the test. We will now use NSubstitute, which makes the task of creating mocks easier. Let us change the code inside the test constructor:

public OutletTest()
{
    var wrapper = Substitute.For<IDateTimeWrapper>();

    sut = new Outlet(wrapper);
}

We are still not specifying the behavior of the wrapper, so both tests are failing. Let’s add this behaviour, so now we can specify what the date is every time we run the test. With NSubstitute again, the code is nice and readable:

public OutletTest()
{
    var wrapper = Substitute.For<IDateTimeWrapper>();

    wrapper.Now().Returns(new DateTime(2019, 4, 28, 17, 00, 00, DateTimeKind.Utc));

    sut = new Outlet(wrapper);
}

So we have specified that every time we run the test, the UTC time is always April 28th 2019 at 17:00. We have gone through all this trouble but still the tests fail. That is because we did not implement any logic in the Outlet class, but now we have done enough groundwork to let us do that efficiently. The main point I wanted to show you was about removing hard dependencies to system objects and now we have achieved that. See you in part 3!

0 comments

Add your comments