Improve Testing: Wrapping Statics

Improve Testing: Wrapping Statics

Most of us are probably familiar with writing unit tests by now. We probably understand that we should keep our code simple, inject our dependencies on external classes, and keep our own classes to a single responsibility.

But we also know that sometimes we need to rely on code that isn't our own. What happens when we only have access to static methods that we can't inject or control? How do we test just our code in this case?

Let's start with a simple example of the problem:

public class BirthdayChecker
{
    public bool IsBirthday(DateOnly date)
    {
        var today = DateTime.UtcNow.Date;

        if(date.Day == today.Day && date.Month == today.Month)
        {
            return true;
        }
        else
        {
            return false;
        }
    }
}

As you can see, this class depends on DateTime.UtcNow, a static method on the System.DateTime class. One outside of our control. Obviously, as it stands, our tests will probably only work once a year - that's not exactly ideal.

Since we can't directly change the DateTime class, we'll have to take a step back and change how we access it instead. To do this, we'll wrap it in a new class, and give it an interface like so:

public interface IDateTimeWrapper
{
    public DateTime UtcNow { get; }
}

public class DateTimeWrapper :  IDateTimeWrapper
{
    public DateTime UtcNow => DateTime.UtcNow;
}

Great! Now we have a very simple wrapper for DateTime. Let's look at using it in our class:

public class BirthdayChecker
{
    private readonly IDateTimeWrapper _dateTime;

    public BirthdayChecker(IDateTimeWrapper dateTimeWrapper)
    {
        _dateTime = dateTimeWrapper;
    }

    public bool IsBirthday(DateOnly date)
    {
        var today = _dateTime.UtcNow.Date;

        if(date.Day == today.Day && date.Month == today.Month)
        {
            return true;
        }
        else
        {
            return false;
        }
    }
}

What we've done here is inject the new interface via our class's constructor, and made it available internally to the class. You'd register this in the usual way with your dependency injection of choice. Most wrappers should be safe to use as Singletons, but you'll want to make sure in your own cases. Additionally, we've replaced any calls to the class with calls to our wrapper instead.

So, how does this help us? The big change we've made here is to transfer control. We've gone from using something we can't control, to something we can. This opens up the opportunity for us to mock or stub this wrapper for our tests to give predictable results and values, that are always the same for our tests. Let's make a stub we can use now:

public class DateTimeStub :  IDateTimeWrapper
{
    private DateTime _dateTime;

    public DateTime UtcNow => _dateTime;

    public DateTimeStub(DateTime dateTime)
    {
        _dateTime = dateTime;
    }
}

There's definitely multiple ways we could write this class, but we've kept it simple here. We implement the interface, and take an instance of DateTime when constructing, and we use this fixed instance each time we call it. We can now just pass this stub instance into our tests to get predictable results:

[Fact]
public void IsBirthday_WhenItIsABirthday_ShouldReturnTrue()
{
    // Arrange
    var dateTimeStub = new DateTimeStub(new DateTime(2024, 2, 26));
    var birthday = new DateOnly(2024, 2, 26);

    var classUnderTest = new BirthdayChecker(dateTimeStub);

    // Act
    var isBirthday = classUnderTest.IsBirthday(birthday);

    // Assert
    Assert.True(isBirthday);
}

Great! Now we can easily test just our BirthdayChecker without having to worry about time advancing, or trying to test code outside of our class in the process.