Testing Date and Time in C#
Nothing is easier than getting the current time in C#. You type DateTime.Now
or DateTime.UtcNow
and you're good to go. But this simplicity has a cost — the code you write in this manner is often hard to understand, change, and test.
Let's see what we can do with that.
The pains of DateTime.Now
Meet Tim. Tim works as a World Time Consultant: he can tell you the current time in any city around the world.
Here is how Tim does his job:
- Tim is given the name of the city.
- Tim has a big book with all the city names and corresponding time zones. He uses it to look up the correct time zone for the given city.
- Fortunately for Tim, he is standing near a Big Clock that always shows the current time; he looks at the clock and notes the current time down.
- Tim then adds or subtracts the number of minutes for the given time zone — and this is how he gets the local time in the given city.
Let's model Tim's job in C#:
public DateTime GetLocalTimeIn(string city) { var timeZone = TimeZoneBook.FindByCity(city); return TimeZoneInfo.ConvertTime(DateTime.Now, timeZone); }
Tim uses three data points to construct his answer: the city name, the time zone book, and the current time.
- The city name is an explicit input that Tim receives from his clients.
- The time zone book is something that no one besides Tim sees or controls. It also doesn't change often, so we can expect it to always give the same time zone for the same city throughout Tim's work shift.
- Lastly, the current time is an input value of which Tim infers himself.
The current time can be problematic because of its two properties:
- It is a hidden input: Tim doesn't receive the current time from his clients; instead, he uses the Big Clock (the static
DateTime.Now
property) to get it. It's impossible to know that Tim needs the current time without knowing how he does his job i.e. without looking into the source code. - The current time is centrally governed. There is only one Big Clock. This is not a problem for Tim, but it can be for people who interact with him.
While Tim enjoys the convenience of looking at the Big Clock to calculate the local time, it complicates the life of Cherise, Tim's supervisor.
Cherise's job is to make sure Tim always gives the correct answer. She asks Tim about the local time and compares it with the result that she thinks is correct. If Cherise had to write a unit test, it'd look similar to this:
var sydneyTime = GetLocalTimeIn("Sydney"); var expectedSydneyTime = DateTime.UtcNow.AddHours(10); Assert.Equal(expectedSydneyTime, sydneyTime, TimeSpan.FromSeconds(1));
While the test looks straightforward, it has two issues:
1. The expected result is not exactly the same as the actual one
Cherise calculates the expected local time after receiving Tim's answer. She also needs to look at the Big Clock for that. Since there's a small delay between Cherise's and Tim's calculations, Cherise has to accept that the answer won't be exactly the same as she expects. In her case, Cherise allows for a small time difference, but that can be an issue in the case of strict precision requirements.
2. The test knowns too much
Cherise has to make an assumption about how Tim does his job. But what else can she do? She must construct the expected answer on the fly just because she cannot tell Tim what the current time is. Cherise cannot possibly climb the clock tower to change the current time just to test Tim — nobody would let her as it would affect everyone else.
That may make Cherise's tests brittle. If she runs her test between April and October, she'd be happy with Tim's answers. But since she's asking about the local time in Sydney, and Sydney switches to Daylight Saving Time in October, her tests will suddenly start failing if run between October and April.
Cherise would have to change her test to add 10 hours to UTC between April and October and 11 hours the rest of the year.
That means that Cherise must know a lot about how time zones work. She no longer treats Tim's job as a black box, she essentially does his job, too. And this leads to her tests not bringing much value. Vladimir Khorikov wrote a great blog post describing this issue in depth.
Better ways to test DateTime
We've just seen how challenging it is to test the code that depends on the static DateTime.Now
property because it is a hidden read-only input. Let's now try to make Cherise's and Tim's lives easier.
1. Basic Ambient Context
The company where Tim and Cherise work bought a Smaller Clock. The time on this clock can be changed and even stopped to help supervisors like Cherise do their testing. After the testing is done, the time on the Smaller Clock needs to be reset, so everyone else can use it to get the current time.
This is how such a clock can be modeled in C#:
public static class SmallerClock { private static readonly DateTime? _currentTime; public static DateTime Now => _currentTime?.Value ?? DateTime.Now; public static void Set(DateTime currentTime) { _currentTime.Value = currentTime; } public static void Reset() { _currentTime.Value = null; } }
Tim instantly learned how to use the Smaller Clock instead of the Big one. This is how he does his job now:
public DateTime GetLocalTimeIn(string city) { var timeZone = TimeZoneBook.FindByCity(city); return TimeZoneInfo.ConvertTime(SmallerClock.Now, timeZone); }
For Cherise, the Smaller Clock was a huge improvement.
Or, written in C#:
SmallerClock.Set(new DateTime(2023, 7, 15, 10, 35, 11, DateTimeKind.Local)); var sydneyTime = GetLocalTimeIn("Sydney"); var expectedSydneyTime = new DateTime(2023, 7, 15, 19, 35, 11, DateTimeKind.Local); Assert.Equal(expectedSydneyTime, sydneyTime); SmallerClock.Reset();
To test Tim, Cherise now first needs to set the specific time on the Smaller Clock, then ask Tim for an answer, and compare that answer to the one she expects. Lastly, she needs to reset the Smaller Clock so it starts showing the real time again. Cherise's life is much better now:
- She no longer needs to know the details of how time zones work. She just knows that the given local time, converted to a specific time zone, should equal another time.
- The time returned by Tim is now exactly the same as the one Cherise expects.
There are, however, downsides to using the Smaller Clock:
- Cherise needs to remember to reset the clock after each test so that everyone else who's using it gets the current time.
- Since Tim can get the current time from the Smaller Clock, nothing forbids him from setting the current time, even by mistake. We've given Tim — our production code — access to the functionality that is needed only for testing.
- If Tim's company decides to run multiple parallel training sessions for other employees, everyone's in for a big surprise.
Let's talk about the third issue. Meet two more people: Vira, Tim's colleague, and Bob, Vira's supervisor.
If Cherise and Bob decide to test Tim and Vira at the same time, bad things can happen:
Here's what happened: Cherise had set the Smaller Clock for testing Tim and asked him for an answer. However, right in the middle of her doing that, Bob also set the Smaller Clock to test Vira. When calculating the local time, Tim based it on the time set by Bob, not Cherise. The answer was technically correct, but Cherise expected it to be different.
This kind of an issue caused by parties that use a shared mutable resource in parallel is called a race condition. Race conditions can be hard to detect and fix as they are unpredictable.
For the testing with Smaller Clock to work, everyone would need to agree to do it in sequence. That means, no running tests in parallel. And that can be slow.
Is there a better solution?
2. Time as an explicit dependency
The company made another investment — they bought watches for their employees and taught everyone how to use them.
All watches do the same thing — they display time. Or, in C#:
public interface IWatch { DateTime Now { get; } }
Before each shift, Tim and Vira each get a watch that displays the current time:
public class RealWatch: IWatch { public DateTime Now => DateTime.Now; }
Cherise and Bob each get a special type of watch — the one that can be set to display a predefined time:
public class SpecialTestingWatch: IWatch { public DateTime Now { get; set; } public SpecialTestingWatch(DateTime value) { Now = value; } }
Here's how Tim and Vira would use their watches:
public class LocalTimeConverter { private IWatch _watch; public LocalTimeConverter(IWatch watch) { _watch = watch; } public DateTime GetLocalTimeIn(string city) { var timeZone = TimeZoneBook.FindByCity(city); return TimeZoneInfo.ConvertTime(_watch.Now, timeZone); } }
And here's how Cherise and Bob would test their colleagues:
Or, in C#:
var watch = new SpecialTestingWatch(new DateTime(2023, 7, 15, 10, 35, 11, DateTimeKind.Local)); var converter = new LocalTimeConverter(watch); var sydneyTime = converter.GetLocalTimeIn("Sydney"); var expectedSydneyTime = new DateTime(2023, 7, 15, 19, 35, 11, DateTimeKind.Local); Assert.Equal(expectedSydneyTime, sydneyTime);
This approach is much better than using either a Big or Small clock:
- The code receives a watch — a time provider — as an explicit dependency. It makes it clear what inputs are required to produce an output.
- The tests don't need to know how the code works. They simply set preconditions and verify the results.
- Since each
LocalTimeConverter
receives its own instance of theIWatch
, the watches are not shared. This means that the tests can now run in parallel.
Resolving the current time using an explicit dependency works really well when used in the orchestration code — think controllers, handlers, and services.
It may, however, not be the best approach when writing domain code — the code that implements business logic — the heart of your software. Why is it so?
Domain code should ideally be decoupled from the other parts of your app. It should also depend only on its own state and that state should be explicit and known in advance.
The time returned by the time provider that is injected as an explicit dependency originates outside of the boundaries of your domain. Besides that, the current time value is not passed explicitly. Here is another great post from Vladimir Khorikov that explains this issue in more depth.
So, what to do if you want to write domain code that needs to know the current time?
3. Injecting time as plain value
What if there was a device that would do the time conversion automatically? You give it the current time and the name of the city and it displays the local time in that city's time zone:
public class ZonedDateTime { public DateTime Value { get; } public ZonedDateTime(string city, DateTime currentTime) { var timeZone = TimeZoneBook.FindByCity(city); Value = TimeZoneInfo.ConvertTime(currentTime, timeZone); } }
We've just created an object that has clear and explicit inputs. It's also easy for Cherise to test it:
In code, the test would look like this:
var now = new DateTime(2023, 7, 15, 10, 35, 11, DateTimeKind.Local); var sydneyTime = new ZonedDateTime("Sydney", now); var expectedSydneyTime = new DateTime(2023, 7, 15, 19, 35, 11, DateTimeKind.Local); Assert.Equal(expectedSydneyTime, sydneyTime.Value);
The test is:
- Deterministic. You will always receive a predictable result when passing the same date and time value.
- Simple. You don't need an additional time provider or a static class.
- Safe to run in parallel with other tests.
When writing domain code that depends on the current time, consider passing it by value. But where would that value originate from? It may not be convenient for the users of the system to calculate the current time themselves.
You'd get it from the time provider injected as a dependency in your controllers or handlers. In our case, Tim will act as such a controller:
When Tim is asked about the current time in a specific time zone, he gets the current local time from an explicit dependency, IWatch
, then passes it to ZonedDateTime
, and uses it to return his answer. In C#, that would look like this:
public class LocalTimeConverter { private IWatch _watch; public LocalTimeConverter(IWatch watch) { _watch = watch; } public DateTime GetLocalTimeIn(string city) { return new ZonedDateTime(city, _watch.Now).Value; } }
So far we've explored using an Ambient Context, injecting an explicit dependency, and passing the date and time by value. Are there any other approaches? Turns out, there is one more — it involves using a special version of an Ambient Context.
Brace yourself for a trippy example.
4. Async-aware Ambient Context
Do you remember the race condition issue that we discovered when talking about the basic Ambient Context earlier in this post? Bob and Cherise used the Smaller Clock in their testing, but Bob was interfering with Cherise's results when they both tried to set the clock at the same time.
Since there is a single shared instance of the Smaller Clock, the only way for everyone to use it is to line up and do testing sequentially.
There is only one thing that could solve this problem — multiverse!
Imagine if Cherise and Bob would use the same Smaller Clock to test Tim and Vira, and they would do it at the same time, but in parallel universes.
Setting the Smaller Clock in one universe won't set it in another — each universe will be implicitly isolated from another.
Still with me? Let's model this in C#:
public static class MultiverseSmallerClock { private static readonly AsyncLocal<DateTime?> CurrentTime = new(); public static DateTime Now => CurrentTime.Value ?? DateTime.Now; public static void Set(DateTime currentTime) { CurrentTime.Value = currentTime; } public static void Reset() { CurrentTime.Value = null; } }
This looks almost the same as the SmallerClock
we've seen before. The only difference here is that the CurrentTime
field is a DateTime
wrapped in the AsyncLocal
class. AsyncLocal
does all the magic here: it isolates the value of the CurrentTime
to an asynchronous flow. In simple words, your tests running in parallel will each have their own value of the CurrentTime
field despite using the same MultiverseSmallerClock
.
That would allow you to swap the calls to DateTime.Now
with MultiverseSmallerClock.Now
and test the code the same way we've seen in the Ambient Context example.
This approach has its benefits:
- Your code will get the exact date and time you set in your tests.
- You will be able to run your tests in parallel.
- It requires a minimal change in code.
But there are two drawbacks:
- The dependency on the current time is still implicit. You cannot tell that your code needs the current time by looking at its signature.
- If used in the domain layer, you forfeit domain purity by having your domain depend on the out-of-process dependency.
So, is it worth using such an Ambient Context at all? Yes, it is a good option when:
- You want to cover existing code with tests without doing massive refactoring. You only need to change the calls to
DateTime.Now
without the need to touch the signatures of the methods. It can be a good way to cover your code with tests before refactoring it to use a date and time provider or to accept DateTime as a value. - Your methods have a layer of indirection, such as a state machine with callbacks that utilize the current time. You may decide not to propagate the current time all the way to the places it's needed and opt for receiving it from an implicit dependency.
Introducing Timecop
Generally, you don't need to write a lot of code to use any of the above approaches. However, you can find yourself doing the same work in different codebases. That's why I have created a small tool called Timecop which helps you test the code that accepts DateTime
from an explicit dependency or from an async-aware Ambient Context. If you use NodaTime, then there's also a Timecop for that, called Timecop.NodeTime.
Summary
We have seen that the code that gets the current time from the DateTime.Now
or DateTime.UtcNow
properties can be hard to test, maintain, and understand. We've also explored several approaches to dealing with such code and came up with the following heuristics:
- In your controllers and handlers, get the current time from a provider that is passed as an explicit dependency.
- In your domain code, accept the current time as a parameter.
- Consider getting the current time from an async-aware Ambient Context if you want to minimize the refactoring impact when covering existing code with tests or when there's a significant layer of indirection that makes passing
DateTime
as a value inconvenient.