In this article, we’ll look closer at how we can overwrite the DateTime.Now property when testing applications in .NET.
Let’s dive in!
Setup to Overwrite DateTime.Now During Testing in .NET
Before we can start testing, we need to create our code base:
public class Flight { public required string From { get; set; } public required string To { get; set; } public required DateTime Time { get; set; } }
First, we create a simple Flight
class that has required properties for the start point and destination as well as a departure time.
Now, let’s create a simple application that will send messages to passengers:
public static class Messages { public const string FlightDepartsInMoreThanTwoHours = "Flight from {0} to {1} departs in more than 2 hours."; public const string FlightDepartsInLessThanTwoHours = "Flight from {0} to {1} departs in less than 2 hours."; public const string FlightDepartsInLessThanOneHour = "Flight from {0} to {1} departs in less than 1 hour."; public const string FlightDepartsInLessThanThirtyMinutes = "Flight from {0} to {1} departs in less than 30 minutes."; public const string FlightAlreadyDeparted = "Flight from {0} to {1} has already departed."; }
Here, we declare a static Messages
class, which holds various messages that we’ll use to notify users about the departure time of their flight.
Now, we need a service to pick the appropriate message:
public class FlightNotificationService : IFlightNotificationService { public string GetFlightNotificationMessage(Flight flight) { var now = DateTime.Now; var timeToDeparture = (flight.Time - now).TotalHours; return timeToDeparture switch { var x when x >= 2 => string.Format(Messages.FlightDepartsInMoreThanTwoHours, flight.From, flight.To), var x when x <= 2 && x > 1 => string.Format(Messages.FlightDepartsInLessThanTwoHours, flight.From, flight.To), var x when x <= 1 && x > 0.5 => string.Format(Messages.FlightDepartsInLessThanOneHour, flight.From, flight.To), var x when x <= 0.5 && x > 0 => string.Format(Messages.FlightDepartsInLessThanThirtyMinutes, flight.From, flight.To), _ => string.Format(Messages.FlightAlreadyDeparted, flight.From, flight.To) }; } }
We define our FlightNotificationService
class. We create one method that checks the time left to the departure of a given Flight
instance and returns the corresponding message.
Next, we create a new xUnit test project using the dotnet new xunit
CLI command:
dotnet new xunit -o OverwriteDateTimeDuringTesting.Tests
Finally, we can write our first test:
[Fact] public void GivenMoreThanTwoHours_WhenGetFlightNotificationMessageIsInvoked_ThenCorrectMessageIsReturned() { // Arrange var flight = new Flight { From = "Heathrow Airport", To = "John F. Kennedy International Airport", Time = new DateTime(2024, 02, 05, 15, 00, 00) }; var expectedMessage = string.Format(Messages.FlightDepartsInMoreThanTwoHours, flight.From, flight.To); var sut = new FlightNotificationService(); // Act var result = sut.GetFlightNotificationMessage(flight); // Assert result.Should().Be(expectedMessage); }
Here, we create a test method to verify that we receive the correct message if our flight departs in more than two hours. Inside the test method, we create a new flight that departs at 15:00 on February 5th, 2024. Our test passes as long as we run it before 12:59 on February 5th, 2024 and this is far from ideal.
The GetFlightNotificationMessage()
method is very simple but it has one major problem. It’s tightly coupled and relies on external dependency, in our case the DateTime.Now
property. This breaks the dependency inversion principle and is a major red flag. If we write unit tests, DateTime.Now
will always return the time when our tests run, they will be flaky at best – they will pass one time and fail another.
Let’s see how we can improve and test our code!
How to Overwrite DateTime.Now During Testing With a Custom Provider in .NET
To implement the dependency inversion principle, we need to have a way to pass some sort of time provider to our class. There are external libraries such as Noda Time but another common approach is writing a custom provider:
public interface ICustomTimeProvider { DateTime Now { get; } }
Here, we create the ICustomTimeProvider
interface that has a Now
property that returns a DateTime
instance.
Now, we can implement the interface:
public class CustomTimeProvider : ICustomTimeProvider { public DateTime Now => DateTime.Now; }
Here, we create the CustomTimeProvider
class and implement the ICustomTimeProvider
interface. For the Now
property, we just return DateTime.Now
. This might seem like extra work to just return the same thing, but this way we can easily mock what the Now
property returns during testing.
There is one final thing to do before updating our tests:
public class FlightNotificationService(ICustomTimeProvider timeProvider) : IFlightNotificationService { public string GetFlightNotificationMessage(Flight flight) { var now = timeProvider.Now; // code removed for brevity } }
Here we use a primary constructor to make our FlightNotificationService
class require an ICustomTimeProvider
instance when during instantiation.
Next, we can update our test class to use our custom provider:
public class FlightNotificationServiceTests { private readonly ICustomTimeProvider _timeProvider; public FlightNotificationServiceTests() { _timeProvider = Substitute.For<ICustomTimeProvider>(); } }
We create a ICustomTimeProvider
field and initialize it in the constructor using the NSubstitute library.
And finally, we update our test method to use this provider:
[Fact] public void GivenMoreThanTwoHours_WhenGetFlightNotificationMessageIsInvoked_ThenCorrectMessageIsReturned() { // Arrange var flight = new Flight { From = "Heathrow Airport", To = "John F. Kennedy International Airport", Time = new DateTime(2024, 02, 05, 15, 00, 00) }; _timeProvider.Now.Returns(new DateTime(2024, 02, 05, 10, 00, 00)); var expectedMessage = string.Format(Messages.FlightDepartsInMoreThanTwoHours, flight.From, flight.To); var sut = new FlightNotificationService(_timeProvider); // Act var result = sut.GetFlightNotificationMessage(flight); // Assert result.Should().Be(expectedMessage); }
First, we set the return value of _timeProvider.Now
to always be 10:00 on February 5th, 2024. The second thing is to use the _timeProvider
field when initializing the FlightNotificationService
class. The rest of the test method remains unchanged and it will always pass as we have now “frozen” the time.
Now, we can write test methods to cover all potential cases of the GetFlightNotificationMessage()
method.
How to Overwrite DateTime.Now During Testing With a Built-in Provider in .NET
With .NET 8, we receive a built-in way to achieve what we did with our ICustomTimeProvider
interface. It comes in the form of the time provider class with the fitting name of TimeProvider
.
To use it in our class, we need to change two things:
public class FlightNotificationService(TimeProvider timeProvider) : IFlightNotificationService { public string GetFlightNotificationMessage(Flight flight) { var now = timeProvider.GetLocalNow(); // removed for brevity } }
In our FlightNotificationService
class, we change ICustomTimeProvider
to TimeProvider
in the constructor. Then we call the GetLocalNow()
method to get the equivalent of DateTime.Now
from the TimeProvider
class.
There is one more thing we need to do:
builder.Services.AddSingleton(TimeProvider.System);
In our Program
class, we register the TimeProvider
provider as a singleton using its System
property – this will make it return values based on our system’s time.
Next, we move on to our test class:
public class FlightNotificationServiceTests { private readonly FakeTimeProvider _timeProvider; public FlightNotificationServiceTests() { _timeProvider = new FakeTimeProvider(); } }
We can again use NSubstitute to mock time, but with TimeProvider
we also get a FakeTimeProvider
class located in the Microsoft.Extensions.Time.Testing
NuGet package.
Now, we have one final change to make to our test:
[Fact] public void GivenMoreThanTwoHours_WhenGetFlightNotificationMessageIsInvoked_ThenCorrectMessageIsReturned() { // Arrange var flight = new Flight { From = "Heathrow Airport", To = "John F. Kennedy International Airport", Time = new DateTime(2024, 02, 05, 15, 00, 00) }; _timeProvider.SetUtcNow(new DateTime(2024, 02, 05, 10, 00, 00)); var expectedMessage = string.Format(Messages.FlightDepartsInMoreThanTwoHours, flight.From, flight.To); var sut = new FlightNotificationService(_timeProvider); // Act var result = sut.GetFlightNotificationMessage(flight); // Assert result.Should().Be(expectedMessage); }
In our test method, we use the SetUtcNow()
method to configure what the FakeTimeProvider
class will return when it’s UtcNow()
method is invoked. This is the only change we need to make our test work with the built-in time provider.
Conclusion
In this article, we saw that overcoming the challenges of testing time-dependent functionality in .NET applications requires strategic design. By implementing the dependency inversion principle and utilizing custom or built-in time providers, we can decouple our code from DateTime.Now, ensuring reliable and consistent tests. With this approach we foster maintainability and stability, addressing the inherent issues associated with time-sensitive testing scenarios systematically and efficiently.