A short exploration of replacing the system clock with Test Doubles.

In a comment to my article Waiting to never happen, Laszlo asks:

"Why have you decided to make the date of the reservation relative to the SystemClock, and not the other way around? Would it be more deterministic to use a faked system clock instead?"

The short answer is that I hadn't thought of the alternative. Not in this context, at least.

It's a question worth exploring, which I will now proceed to do.

Why IClock? #

The article in question discusses a unit test, which ultimately arrives at this:

[Fact]
public async Task ChangeDateToSoldOutDate()
{
    var r1 =
        Some.Reservation.WithDate(DateTime.Now.AddDays(8).At(20, 15));
    var r2 = r1
        .WithId(Guid.NewGuid())
        .TheDayAfter()
        .WithQuantity(10);
    var db = new FakeDatabase();
    db.Grandfather.Add(r1);
    db.Grandfather.Add(r2);
    var sut = new ReservationsController(
        new SystemClock(),
        new InMemoryRestaurantDatabase(Grandfather.Restaurant),
        db);
 
    var dto = r1.WithDate(r2.At).ToDto();
    var actual = await sut.Put(r1.Id.ToString("N"), dto);
 
    var oRes = Assert.IsAssignableFrom<ObjectResult>(actual);
    Assert.Equal(
        StatusCodes.Status500InternalServerError,
        oRes.StatusCode);
}

The keen reader may notice that the test passes a new SystemClock() to the sut. In case you're wondering what that is, here's the definition:

public sealed class SystemClock : IClock
{
    public DateTime GetCurrentDateTime()
    {
        return DateTime.Now;
    }
}

While it should be possible to extrapolate the IClock interface from this code snippet, here it is for the sake of completeness:

public interface IClock
{
    DateTime GetCurrentDateTime();
}

Since such an interface exists, why not use it in unit tests?

That's possible, but I think it's worth highlighting what motivated this interface in the first place. If you're used to a certain style of test-driven development (TDD), you may think that interfaces exist in order to support TDD. They may. That's how I did TDD 15 years ago, but not how I do it today.

The motivation for the IClock interface is another. It's there because the system clock is a source of impurity, just like random number generators, database queries, and web service invocations. In order to support repeatable execution, it's useful to log the inputs and outputs of impure actions. This includes the system clock.

The IClock interface doesn't exist in order to support unit testing, but in order to enable logging via the Decorator pattern:

public sealed class LoggingClock : IClock
{
    public LoggingClock(ILogger<LoggingClock> logger, IClock inner)
    {
        Logger = logger;
        Inner = inner;
    }
 
    public ILogger<LoggingClock> Logger { get; }
    public IClock Inner { get; }
 
    public DateTime GetCurrentDateTime()
    {
        var output = Inner.GetCurrentDateTime();
        Logger.LogInformation(
            "{method}() => {output}",
            nameof(GetCurrentDateTime),
            output);
        return output;
    }
}

All code in this article originates from the code base that accompanies Code That Fits in Your Head.

The web application is configured to decorate the SystemClock with the LoggingClock:

services.AddSingleton<IClock>(sp =>
{
    var logger = sp.GetService<ILogger<LoggingClock>>();
    return new LoggingClock(logger, new SystemClock());
});

While the motivation for the IClock interface wasn't to support testing, now that it exists, would it be useful for unit testing as well?

A Stub clock #

As a first effort, we might try to add a Stub clock:

public sealed class ConstantClock : IClock
{
    private readonly DateTime dateTime;
 
    public ConstantClock(DateTime dateTime)
    {
        this.dateTime = dateTime;
    }
 
    // This default value is more or less arbitrary. I chose it as the date
    // and time I wrote these lines of code, which also has the implication
    // that it was immediately a time in the past. The actual value is,
    // however, irrelevant.
    public readonly static IClock Default =
        new ConstantClock(new DateTime(2022, 6, 19, 9, 25, 0));
 
    public DateTime GetCurrentDateTime()
    {
        return dateTime;
    }
}

This implementation always returns the same date and time. I called it ConstantClock for that reason.

It's trivial to replace the SystemClock with a ConstantClock in the above test:

[Fact]
public async Task ChangeDateToSoldOutDate()
{
    var clock = ConstantClock.Default;
    var r1 = Some.Reservation.WithDate(
        clock.GetCurrentDateTime().AddDays(8).At(20, 15));
    var r2 = r1
        .WithId(Guid.NewGuid())
        .TheDayAfter()
        .WithQuantity(10);
    var db = new FakeDatabase();
    db.Grandfather.Add(r1);
    db.Grandfather.Add(r2);
    var sut = new ReservationsController(
        clock,
        new InMemoryRestaurantDatabase(Grandfather.Restaurant),
        db);
 
    var dto = r1.WithDate(r2.At).ToDto();
    var actual = await sut.Put(r1.Id.ToString("N"), dto);
 
    var oRes = Assert.IsAssignableFrom<ObjectResult>(actual);
    Assert.Equal(
        StatusCodes.Status500InternalServerError,
        oRes.StatusCode);
}

As you can see, however, it doesn't seem to be enabling any simplification of the test. It still needs to establish that r1 and r2 relates to each other as required by the test case, as well as establish that they are valid reservations in the future.

You may protest that this is straw man argument, and that it would make the test both simpler and more readable if it would, instead, use explicit, hard-coded values. That's a fair criticism, so I'll get back to that later.

Fragility #

Before examining the above criticism, there's something more fundamental that I want to get out of the way. I find a Stub clock icky.

It works in this case, but may lead to fragile tests. What happens, for example, if another programmer comes by and adds code like this to the System Under Test (SUT)?

var now = Clock.GetCurrentDateTime();
// Sabotage:
while (Clock.GetCurrentDateTime() - now < TimeSpan.FromMilliseconds(1))
{ }

As the comment suggests, in this case it's pure sabotage. I don't think that anyone would deliberately do something like this. This code snippet even sits in an asynchronous method, and in .NET 'everyone' knows that if you want to suspend execution in an asynchronous method, you should use Task.Delay. I rather intend this code snippet to indicate that keeping time constant, as ConstantClock does, can be fatal.

If someone comes by and attempts to implement any kind of time-sensitive logic based on an injected IClock, the consequences could be dire. With the above sabotage, for example, the test hangs forever.

When I originally refactored time-sensitive tests, it was because I didn't appreciate having such ticking bombs lying around. A ConstantClock isn't ticking (that's the problem), but it still seems like a booby trap.

Offset clock #

It seems intuitive that a clock that doesn't go isn't very useful. Perhaps we can address that problem by setting the clock back. Not just a few hours, but days or years:

public sealed class OffsetClock : IClock
{
    private readonly TimeSpan offset;
 
    private OffsetClock(DateTime origin)
    {
        offset = DateTime.Now - origin;
    }
 
    public static IClock Start(DateTime at)
    {
        return new OffsetClock(at);
    }
 
    // This default value is more or less arbitrary. I just picked the same
    // date and time as ConstantClock (which see).
    public readonly static IClock Default =
        Start(at: new DateTime(2022, 6, 19, 9, 25, 0));
 
    public DateTime GetCurrentDateTime()
    {
        return DateTime.Now - offset;
    }
}

An OffsetClock object starts ticking as soon as it's created, but it ticks at the same pace as the system clock. Time still passes. Rather than a Stub, I think that this implementation qualifies as a Fake.

Using it in a test is as easy as using the ConstantClock:

[Fact]
public async Task ChangeDateToSoldOutDate()
{
    var clock = OffsetClock.Default;
    var r1 = Some.Reservation.WithDate(
        clock.GetCurrentDateTime().AddDays(8).At(20, 15));
    var r2 = r1
        .WithId(Guid.NewGuid())
        .TheDayAfter()
        .WithQuantity(10);
    var db = new FakeDatabase();
    db.Grandfather.Add(r1);
    db.Grandfather.Add(r2);
    var sut = new ReservationsController(
        clock,
        new InMemoryRestaurantDatabase(Grandfather.Restaurant),
        db);
 
    var dto = r1.WithDate(r2.At).ToDto();
    var actual = await sut.Put(r1.Id.ToString("N"), dto);
 
    var oRes = Assert.IsAssignableFrom<ObjectResult>(actual);
    Assert.Equal(
        StatusCodes.Status500InternalServerError,
        oRes.StatusCode);
}

The only change from the version that uses ConstantClock is the definition of the clock variable.

This test can withstand the above sabotage, because time still passes at normal pace.

Explicit dates #

Above, I promised to return to the criticism that the test is overly abstract. Now that it's possible to directly control time, perhaps it'd simplify the test if we could use hard-coded dates and times, instead of all that relative-time machinery:

[Fact]
public async Task ChangeDateToSoldOutDate()
{
    var r1 = Some.Reservation.WithDate(
        new DateTime(2022, 6, 27, 20, 15, 0));
    var r2 = r1
        .WithId(Guid.NewGuid())
        .WithDate(new DateTime(2022, 6, 28, 20, 15, 0))
        .WithQuantity(10);
    var db = new FakeDatabase();
    db.Grandfather.Add(r1);
    db.Grandfather.Add(r2);
    var sut = new ReservationsController(
        OffsetClock.Start(at: new DateTime(2022, 6, 19, 13, 43, 0)),
        new InMemoryRestaurantDatabase(Grandfather.Restaurant),
        db);
 
    var dto = r1.WithDate(r2.At).ToDto();
    var actual = await sut.Put(r1.Id.ToString("N"), dto);
 
    var oRes = Assert.IsAssignableFrom<ObjectResult>(actual);
    Assert.Equal(
        StatusCodes.Status500InternalServerError,
        oRes.StatusCode);
}

Yeah, not really. This isn't worse, but neither is it better. It's the same size of code, and while the dates are now explicit (which, ostensibly, is better), the reader now has to deduce the relationship between the clock offset, r1, and r2. I'm not convinced that this is an improvement.

Determinism #

In the original comment, Laszlo asked if it would be more deterministic to use a Fake system clock instead. This seems to imply that using the system clock is nondeterministic. Granted, it is when not used with care.

On the other hand, when used as shown in the initial test, it's almost deterministic. What time-related circumstances would have to come around for the test to fail?

The important precondition is that both reservations are in the future. The test picks a date eight days in the future. How might that precondition fail?

The only failure condition I can think of is if test execution somehow gets suspended after r1 and r2 are initialised, but before calling sut.Put. If you run the test on a laptop and put it to sleep for more than eight days, you may be so extremely lucky (or unlucky, depending on how you look at it) that this turns out to be the case. When execution resumes, the reservations are now in the past, and sut.Put will fail because of that.

I'm not convinced that this is at all likely, and it's not a scenario that I'm inclined to take into account.

And in any case, the test variation that uses OffsetClock is as 'vulnerable' to that scenario as the SystemClock. The only Test Double not susceptible to such a scenario is ConstantClock, but as you have seen, this has more immediate problems.

Conclusion #

If you've read or seen a sufficient amount of time-travel science fiction, you know that it's not a good idea to try to change time. This also seems to be the case here. At least, I can see a few disadvantages to using Test Double clocks, but no clear advantages.

The above is, of course, only one example, but the concern of how to control the passing of time in unit testing isn't new to me. This is something that have been an issue on and off since I started with TDD in 2003. I keep coming back to the notion that the simplest solution is to use as many pure functions as possible, combined with a few impure actions that may require explicit use of dates and times relative to the system clock, as shown in previous articles.


Comments

I agree to most described in this post. However, I still find StubClock as my 'default' approach. I summarized the my reasons in this gist reply.

2022-06-30 7:43 UTC


Wish to comment?

You can add a comment to this post by sending me a pull request. Alternatively, you can discuss this post on Twitter or somewhere else with a permalink. Ping me with the link, and I may respond.

Published

Monday, 27 June 2022 05:44:00 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!
Published: Monday, 27 June 2022 05:44:00 UTC