Skip to content

Instantly share code, notes, and snippets.

@ladeak
Last active June 29, 2022 20:07
Show Gist options
  • Save ladeak/4f6ece31e941e28bafa1fca844d9fe3b to your computer and use it in GitHub Desktop.
Save ladeak/4f6ece31e941e28bafa1fca844d9fe3b to your computer and use it in GitHub Desktop.
SystemClock and StubClock

Thank you for the longer answer on my question. I choose to come back here with a longer reply because it feels a more appropriate form. After reading your post I still feel using a StubClock with constant value is my 'default' choice. I write this reply so that myself also understand why that would be the case.

Context

  • You likely have more experience working with DateTime
  • Maybe I have been exposed to a different type of business processes using DateTime that implies different usage
  • Maybe there is a pendulum swing waiting for me to happen

Determinism

In case of the test you have described in your post, adding 8 days to the current time creates a moment so far in the future, that it makes the test quite resilient against a lot of issues. However, my experience tells me that in practice usually these timespans are smaller in duration. For example, one hour or less. Such a shorter timespan enables significantly more errors to pop up. One such error from my experience is due to DST change and build agents doing 'builds and tests' nightly. A second case I recall vividly is a 'flaky' test which was broken for multiple days twice a year for developers in Europe but not in the US: because US and EU has the DST time switch on different days.

An example

However, this still does not explain why I would opt for StubClock by default.

One thing I notice now that I have never had issues with the notion of time passing by in tests. That means that in most cases, I am OK with using a StubClock with a constant value in my tests.

Here, I try to describe an example test, which put me on the path of using a constant StubClock early in my career. This is an artificial example; I do not have access to the original context of the problem anymore - so excuse me for the rough edges. Let us assume we have a desktop application used by users who send orders to stock exchanges. For example, they could send an order to NYSE or to FTSE. Exchanges are open in a well-defined time. When a user sends an order, but the exchange is closed, the order is put on hold until the next day the exchange opens for trading. In such a case the application shows in the user's timezone when the order will get processed. For example, if user in Frankfurt sends an order at 10am CEST to NYSE which only opens at 9 AM EDT, the application will show a DateTime of 3pm CEST.

Let us also assume exchanges publish metadata of opening and closing hours:

{ "Name": "NYSE", "OpenHour": 9, "OpenMinute": 0, "CloseHour": 17, "CloseMinute": 0, "Timezone": "US Eastern Standard Time" }

When a user selects to send an order, the corresponding exchange is passed to a method which calculates when the given order would be processed by the exchange. To support such a feature a developer could come up with a method interface as follows:

public interface IExchangeSessionConverter
{
    public DateTime GetExchangeSession(IClock clock, ExchangeMetadata exchange);
}

public class ExchangeMetadata
{
    public string Name { get; set; }
    public int OpenHour { get; set; }
    public int OpenMinute { get; set; }
    public int CloseHour { get; set; }
    public int CloseMinute { get; set; }
    public string Timezone { get; set; }
}

// For reference my IClock interface:
public interface IClock
{
    public DateTime Now { get; }
    public TimeZoneInfo Timezone { get; }
}

GetExchangeSession returns the DateTime indicating when the order would be executed. IClock does not need to be parameter, it could be injected to the actual implementation of IExchangeSessionConverter but I choose this for brevity. An implementation of GetExchangeSession method would need to create an opening and closing DateTime in the timezone of the exchange. Convert the current time and timezone of the user to the exchange's timezone so that the two times may be compared. There are several possible outcomes of this comparison, which all worth to be covered by a test. Let me write one test for the case when an order is sent before the exchange is open.

Using StubClock

Using StubClock I can define what is 'now' including the timezone. A timezone with opening and closing hours also defined for the exchange. The system under test is executed and the result value is compared with the expected time.

public void StubClock_SameDayOrderBeforeOpen_ReturnsOpeningTime()
{
    var now = new DateTime(2022, 06, 28, 14, 0, 0);
    var clock = new StubClock(now, TimeZoneInfo.FindSystemTimeZoneById(CEST));
    var exchange = new ExchangeMetadata() { OpenHour = 9, OpenMinute = 0, CloseHour = 17, CloseMinute = 0, Timezone = EDT };
    var sut = new ExchangeSessionConverter();
    var result = sut.GetExchangeSession(clock, exchange);
    Assert.Equal(new DateTime(2022, 06, 28, 15, 0, 0), result);
}

Pros/Cons:

  • +I can define the timezone of the user explicitly, and it could be easily parameterized
  • +Time does not progress during debugging
  • +I do not need to worry on minutes, seconds, days, I can clearly focus the hours, and drive the reader's (debugger's) attention to the same
  • -The relation between OpenHour = 9, now and expected result time is not explicitly readable in this implementation

Using SystemClock

In case I would use SystemClock I would need set OpenHour, OpenMinute, CloseHour, CloseMinute properties relative to the current time. The same test could be implemented as:

public void SystemClock_SameDayOrderBeforeOpen_ReturnsOpeningTime()
{
    var clock = new SystemClock();
    var now = clock.Now;
    var timeAtExchange = TimeZoneInfo.ConvertTimeFromUtc(TimeZoneInfo.ConvertTimeToUtc(now, clock.Timezone), TimeZoneInfo.FindSystemTimeZoneById(EDT));
    var openHour = timeAtExchange.AddHours(2);
    var closeHour = openHour.AddHours(8);
    var exchange = new ExchangeMetadata() { OpenHour = openHour.Hour, OpenMinute = 0, CloseHour = closeHour.Hour, CloseMinute = 0, Timezone = EDT };
    var sut = new ExchangeSessionConverter();
    var result = sut.GetExchangeSession(clock, exchange);
    Assert.Equal(DateTime.Today.AddHours(now.Hour + 2), result);
}

Pros/Cons:

  • -I cannot define the timezone of the user explicitly, and it could be easily parameterized
  • -Time progress during debugging
  • -I need to worry on minutes, seconds, days, during debugging
  • -Expected time for the assertion needs to be calculated
  • -I have to use a similar logic as in the implementation to convert between time zones (which I find icky) and reduces my confidence in the test due to potential errors
  • +The relation between OpenHour = 9, now and expected result time is explicitly readable in this implementation

Conclusion

After implementing two solutions for this problem, I would possibly prefer a hybrid approach, one where the relation between now, openHour and the testcase is more explicit, while keeping easy debuggability. Unfortunately, I do not see a way to avoid the timezone conversion for such a hybrid approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment