Blog

Dealing With Date/Time In Solitary Tests

August 4, 2020

Solitary tests never cross the process boundary in which they are executed. This means that a solitary test never executes code that talks to a database, communicates across the network, touches the file system, etc. This also implies that a solitary test never deals with the system clock either, whether it’s directly or indirectly.

Let’s have a look at some example code in order to grasp the implications of this.

public class CreateCouponHandler
{
    private readonly ICouponRepository _couponRepository;

    public CreateCouponHandler(ICouponRepository couponRepository)
    {
        _couponRepository = couponRepository;
    }

    public void Handle_Coupled(CreateCoupon command)
    {
        var coupon = new Coupon(command.CouponCode, DateTime.Today);
        _couponRepository.Save(coupon);
    }
}

public class CreateCoupon
{
    public string CouponCode { get; }

    public CreateCoupon(string couponCode)
    {
        CouponCode = couponCode;
    }
}

Here we have a handler that creates a new discount coupon and saves it in the database. A coupon requires a code, and a creation date. The coupon code is specified through an incoming CreateCoupon command object. The creation date is set to the current date by calling the DateTime.Today static property.

Let’s have a look at the test code.

[Specification]
public class When_creating_a_new_coupon
{
    [Establish]
    public void Context()
    {
        _couponRepository = Substitute.For<ICouponRepository>();
        _sut = new CreateCouponHandler(_couponRepository);
    }

    [Because]
    public void Of()
    {
        var command = new CreateCoupon("COUPON_CODE");
        _sut.Handle(command);
    }
    
    [Observation]
    public void Then_a_newly_created_coupon_should_be_saved()
    {
        var expectedCouponToBeSaved = new Coupon("COUPON_CODE", DateTime.Today);
        _couponRepository.Should_have_received(expectedCouponToBeSaved);        
    }

    private ICouponRepository _couponRepository;
    private CreateCouponHandler _sut;
}

According to its definition, this test doesn’t entirely qualify as a solitary test. Sure, it doesn’t require any external configuration file in order for it to execute properly. However, both the implementation and the test code have a direct dependency on the system clock.

This test will pass most of the time. However, on rare occasions this test might also fail. Why? Suppose that this test is being executed on a build server a couple of milliseconds before midnight. The call to DateTime.Today in the Handle method of the CreateCouponHandler class returns the current date. When the test method itself is executed, midnight has already passed. So the call to DateTime.Today returns the date of the next day which causes the test to fail.

I do recognise that the chances are pretty slim for this to actually happen. Nonetheless, this definitely affects the reliability of the test. Suppose that besides the date, we also need to store the time when a coupon gets created. In that case, we would replace the calls to DateTime.Today with DateTime.UtcNow. This would also make the test even less deterministic as it would sometimes fail due to a potential difference of just a couple of milliseconds.

In order to make both the implementation and the test code more loosely coupled, we could remove the direct dependency on the system clock by encapsulating the calls to DateTime.Today and DateTime.UtcNow.

public interface IClock
{
    DateTime GetCurrentDate();
    DateTime GetCurrentDateTime();
}

Here we’ve defined an interface named IClock. This defines a contract for retrieving the current date, or the current date/time.

public class SystemClock : IClock
{
    public DateTime GetCurrentDate()
    {
        return DateTime.Today;
    }

    public DateTime GetCurrentDateTime()
    {
        return DateTime.UtcNow;
    }
}

The SystemClock class provides an implementation of the IClock interface by encapsulating the calls to DateTime.Today and DateTime.UtcNow.

public class CreateCouponHandler
{
    private readonly ICouponRepository _couponRepository;
    private readonly IClock _clock;

    public CreateCouponHandler(ICouponRepository couponRepository, IClock clock)
    {
        _couponRepository = couponRepository;
        _clock = clock;
    }

    public void Handle(CreateCoupon command)
    {
        var creationDate = _clock.GetCurrentDate();
        
        var coupon = new Coupon(command.CouponCode, creationDate);
        _couponRepository.Save(coupon); 
    }
}

An instance of the IClock interface is injected into the constructor of the CreateCouponHandler class. The IoC container of the application will provide the necessary instance of the SystemClock class. In order to get the current date, we just call the GetCurrentDate method.

Let’s see what this means for our test code.

[Specification]
public class When_creating_a_new_coupon
{
    [Establish]
    public void Context()
    {
        _couponRepository = Substitute.For<ICouponRepository>();

        // The clock stub returns a concrete date
        var clock = Substitute.For<IClock>();
        clock.GetCurrentDate().Returns(new DateTime(2020, 08, 01));
        
        _sut = new CreateCouponHandler(_couponRepository, clock);
    }

    [Because]
    public void Of()
    {
        var command = new CreateCoupon("COUPON_CODE");
        _sut.Handle(command);
    }
    
    [Observation]
    public void Then_a_newly_created_coupon_should_be_saved()
    {
        // Here we specify a concrete date for the creation date
        var expectedCouponToBeSaved = new Coupon("COUPON_CODE", new DateTime(2020, 08, 01));
        _couponRepository.Should_have_received(expectedCouponToBeSaved);        
    }

    private ICouponRepository _couponRepository;
    private CreateCouponHandler _sut;
}

Notice that we no longer have a dependency on DateTime.Today inside the test method. Instead, we just specify a concrete date.

var expectedCouponToBeSaved = new Coupon("COUPON_CODE", new DateTime(2020, 08, 01));
_couponRepository.Should_have_received(expectedCouponToBeSaved);

In the setup of the test context, we also specify that a call to the GetCurrentDate method would return the same concrete date.

var clock = Substitute.For<IClock>();
clock.GetCurrentDate().Returns(new DateTime(2020, 08, 01));

By removing the direct dependency on the system clock, we turned our test into a full-fledged solitary test. Not only that. We’ve also made the test itself more deterministic as well. The test no longer suffers from potential failures depending on the time of day that it’s being executed. And then there’s also the aspect of readability.

Making use of DateTime.Today or DateTime.UtcNow lacks readability as a developer has to mentally translate this to a concrete date. Removing this mental translation step just makes things more convenient for other developers on the team. Especially when there’s some kind of date arithmetic involved, making use of a concrete date and/or time communicates more clearly what the inputs are and what the expected outputs should be.

If you and your team want to learn more about how to write maintainable unit tests and get the most out of TDD practices, make sure to have look at our trainings and workshops or check out the books section. Feel free to reach out at infonull@nullprincipal-itnull.be.

Profile picture of Jan Van Ryswyck

Jan Van Ryswyck

Thank you for visiting my blog. I’m a professional software developer since Y2K. A blogger since Y2K+5. Provider of training and coaching in XP practices. Curator of the Awesome Talks list. Past organizer of the European Virtual ALT.NET meetings. Thinking and learning about all kinds of technologies since forever.

Comments

About

Thank you for visiting my website. I’m a professional software developer since Y2K. A blogger since Y2K+5. Author of Writing Maintainable Unit Tests. Provider of training and coaching in XP practices. Curator of the Awesome Talks list. Thinking and learning about all kinds of technologies since forever.

Contact information

(+32) 496 38 00 82

infonull@nullprincipal-itnull.be