2015-06-18

Timetravel in C# done right

Testing things where you need to fake time is not hard given the right abstractions. That is why when I read this article I felt really sad. Because while the article describes one way to fake time it does so by breaking three fundamentals which is pretty hard in three lines of relevant code...

In the linked article the pattern to fake time is to use a static class to wrap calls to DateTime.Now and then uses a TimeSpan property to indicate how to offset time. The fundamental rules broken are; using a static class (test unfriendly construct) to wrap a global value (test unfriendly feature) means if we run multiple tests in parallel we no longer know what is going to happen in the tests. Second the way to manipulate the date which is only needed for tests is now available in production code. Last it uses an offset rather than a fixed value to offset time which means it would be very hard to test something where you actually want exact time values to be returned.

The most important thing to do would be to move away from using a static class. Second use a fixed time or even better a delegate to override default behavior of getting current time. The use of a delegate to fake time is that during the execution of a test you can change what you return very easily.

5 comments:

  1. This is much more robust: http://ayende.com/blog/3408/dealing-with-time-in-tests

    ReplyDelete
    Replies
    1. @Ofer: You are missing the whole point... Your suggested solution also is using a static class to wrap a global value. Better than the the option I linked above but still suffers from the problem of not being able to run multiple tests in parallel.

      Delete
  2. Best solution

    MyMethodThatNeedsTime(DateTime? now = null)

    If(now == null) now = DateTime.Now;

    :drops mic:

    ReplyDelete
    Replies
    1. While I think that is an excellent solution it will not cover all cases. Sometimes the thing being tested actually needs to get time itself rather than having it being handed to it. But you bring up an excellent point. The best way to solve a dependency problem is to remove it. :-)
      :kicks mic off stage:

      Delete
    2. It's interesting how you look at code as you evolve through your career. Several years ago the very first thing i'd have done is just add another constructor injected argument, now i look at doing everything else first before taking another dependency in my constructors.

      Delete