Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using TimeProvider for time mocking #104

Open
wants to merge 3 commits into
base: staging
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 20 additions & 36 deletions Testing/Unit Tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,47 +29,27 @@ public string GetTimeBasedGreeting()

It is easy to think of the test cases for this: we must check for each time of the day that the correct greeting is returned. But as soon as we start writing the tests, we come to an issue: only one greeting can be tested at a certain point in time. This means that we have no way of defining unit tests that will pass every time we run them, even though our code is working perfectly!

The issue here is our static reference to `DateTime.UtcNow`, which returns the current date and time. Unfortunately, despite our incredible technological advancements, we still haven't figured out how to control time. This means that as long as our code is dependent on something as uncontrollable as the unforgiving passage of time, we cannot make it run deterministically.
The issue here is our static reference to `DateTime.UtcNow`, which returns the current date and time.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest merging this sentence with the previous paragraph.


What we _can_ do is add a layer of abstraction between our code and the way we fetch the current time. This can be done by adding an interface that we can easily mock when setting up our tests:
From .NET 8 Microsoft introduced `TimeProvider` abstract class that should replace our custom layer of abstraction for making our code more testable. Previously, we would have something like `IClockProvider` that we would register in the DI container and inject in our service classes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I would also suggest reducing the number of occurrences of the words "our" and 'we". :)


``` c#
public interface ITimeProvider
{
public DateTime UtcNow { get; }
}
```

We must also add an implementation of the interface which will be used in the runtime, which is as simple as a service can be:

``` c#
public class TimeProvider : ITimeProvider
{
public DateTime UtcNow => DateTime.UtcNow;
}
```

In this example, we're keeping it simple by only adding a single property, but we could expand it if we ever needed some other time-related types, like `DateTimeOffset`, or wanted to get the time in some other timezone.

Don't forget to register the service in the DI configuration! This service can be registered as a singleton since it only passes a static reference, so there is no need to create more than one instance of it:
Since the introduction of `TimeProvider` in .NET 8 we get all that out of the box. All we need to do is register the default implementation of the `TimeProvider` as a singleton and inject it in our service and replace direct use of `DateTime.UtcNow`. Default implementation uses current system clock.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about merging this paragraph with the previous one and reworking it to look somewhat like:

"The old approach included the usage of IClockProvider that we would register in the DI container and inject in our service classes. From .NET 8, Microsoft introduced TimeProvider abstract class that should replace the custom layer of abstraction and make the code more testable. All that needs to be done is:

  • registration of the default implementation of the TimeProvider as a singleton
  • its injection in our service
  • replacing the direct usage of DateTime.UtcNow.

The default implementation uses current system clock."


``` c#
services.AddSingleton<ITimeProvider>(new TimeProvider());
```c#
builder.Services.AddSingleton(TimeProvider.System);
```

Lastly, we must replace all `DateTime.UtcNow` references with our new interface:

``` c#
private readonly ITimeProvider _timeProvider;
private readonly TimeProvider _timeProvider;

public GreetingGenerator(ITimeProvider timeProvider)
public GreetingGenerator(TimeProvider timeProvider)
{
_timeProvider = timeProvider;
}

public string GetTimeBasedGreeting()
{
var hourOfTheDay = _timeProvider.UtcNow.Hour;
var hourOfTheDay = _timeProvider.GetUtcNow().Hour;

if (hourOfTheDay >= 6 && hourOfTheDay < 12)
return "Good morning";
Expand All @@ -85,18 +65,22 @@ And that's it, with this simple change we can make this method think it is any t

``` c#
[Theory]
[InlineData(6, 0)]
[InlineData(10, 0)]
[InlineData(11, 59)]
public void GetTimeBasedGreeting_WhenItIsMorning_ThenReturnGoodMorning(
[InlineData(11, 59, "Good morning")]
[InlineData(12, 0, "Good afternoon")]
[InlineData(18, 01, "Good evening")]
public void GetTimeBasedGreeting_WhenTimeAvailable_ThenReturnAppropriateGreeting(
int hour,
int minutes)
int minutes,
string expectedGreeting)
{
_timeProviderMock.Setup(m => m.UtcNow)
_timeProvider
.GetUtcNow()
.Returns(new DateTime(2042, 2, 4, hour, minutes, 0));

var result = _sut.GetTimeBasedGreeting();

result.Should().BeEquivalentTo("Good morning");
result.Should().BeEquivalentTo(expectedGreeting);
}
```
```

TimerProvider class enables us to do much more than just work with current time, so for more examples check out this [article](https://andrewlock.net/exploring-the-dotnet-8-preview-avoiding-flaky-tests-with-timeprovider-and-itimer/).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I recommend replacing "enables us to do much more" with "makes it possible to do much more". Since this is a technical article, I think it's better to be less personal. :)