In this article, we’ll look closer at how we can overwrite the DateTime.Now property when testing applications in .NET.

To download the source code for this article, you can visit our GitHub repository.

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:

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!
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.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!