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.
Subscribe to my newsletter
Read articles from Joshua Moon directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Joshua Moon
Joshua Moon
An AuDHD developer that is probably in need of a bit of refactoring and debugging. I work primarily in C# .NET up and down the stack, but have fingers in several other pies for when I need it. Throw something at me if I get distracted and don't post anything for a while!