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
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.

I think, you overlook an important fact here: It depends™.

As Obi Wan taught us, the point of view is often quite important. In this case, yes it's true, the changed code is more explicit, from a certain point of view, because the dates are now explicit. But: in the previous version, the relationships were explicit, whereas they have been rendered implicit now. Which is better depends on the context, and in this context, I think the change is for the worse.

In this context, I think we care neither about the specific dates nor the relationship between both reservation dates. All we care about is their relationship to the present and that they are different from each other. With that in mind, I'd suggest to extend your Some container with more datetimes, in addition to Now, like FutureDate and OtherFutureDate.

How those are constructed is generally of no relevance to the current test. After all, if we wanted to be 100% sure about every piece, we'd basically have re-write our entire runtime for each test case, which would just be madness. And yes, I'd just construct them based on the current system time.

Regarding the overall argument, I'll say that dealing with time issues is generally a pain, but most of the time, we don't really need to deal with what happens at specific times. In those rare cases, yes, it makes sense to fix the test's time, but I'd leave that as a rare exception. Partly because such tests tend to require some kind of wide-ranging mock that messes with a lot of things.

If we're talking about stuff like Y2k-proofing (if you're too young to remember, look it up, kids), it bears thinking about actually creating a whole test machine (virtual or physical) with an appropriate system time and just running your test suite on there. In times of docker, I'll bet that that will be less pain in many cases than adding time-fixing mock stuff.

If passage of time is important, that's another bag of pain right there, but I'd look into segregating that as much as possible from everything else. If, for example, you need things to happen after 100 minutes have passed, I'd prefer having a single time-based event system that all other code can subscribe to and be called back when the interesting time arrives. That way, I can test the consumers without actually travelling through time, while testing the timer service will be reduced to making sure that events are fired at the appropriate times. The latter could even happen on a persistent test machine that just keeps running, giving you insight on long-time behavior (just an idea, not a prescription 😉).

2024-05-08 11:13 UTC

Thank you for writing. It's a good point that the two alternatives that I compare really only represent different perspectives. As one part becomes more explicit, the other becomes more implicit, and vice versa. I hadn't though of that, so thank you for pointing that out.

Perhaps, as you suggest, a better API might be in order. I'm sure this isn't my last round around that block. I don't, however, want to add Now, FutureDate, etc. to the Some API. This module contains a collection of representative values of various equivalence classes, and in order to ensure test repeatability, they should be immutable and deterministic. This rules out hiding a call to DateTime.Now behind such an API.

That doesn't, however, rule out other types of APIs. If you move to test data generators instead, it might make sense to define a 'future date' generator.

All that said, I agree that the best way to test time-sensitive code is to model it in such a way that it's deterministic. I've touched on this topic before, and most of the tests in the sample code base that accompanies Code That Fits in Your Head takes that approach.

The test discussed in this article, however, sits higher in the Test Pyramid, and for such Facade Tests, I'd like to exercise them in as realistic a context as possible. That's why I run them on the real system clock.

2024-05-18 8:45 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