Thursday, May 28, 2015

Inject DateTime.Now to Aid Unit Tests

If you have logic that relies on the current system date, it's often difficult to see how to unit test it. But by injecting a function that returns DateTime.Now we can stub the current date to be anything we want it to be.
Let's look at an example. Here we have a simple service that creates a new user instance and saves it in a database:
    public class UserService : IUserService
    {
        private readonly IUserData userData;

        public UserService(IUserData userData)
        {
            this.userData = userData;
        }

        public void CreateUser(string username)
        {
            var user = new User(username, createdDateTime: DateTime.UtcNow);
            userData.SaveUser(user);
        }
    }
Now if I want to write a unit test that checks that the correct created date is set, I have to rely on the assumption that the system date won't change between the creation of the User instance and the test assertions.
    [TestFixture]
    public class UserServiceTests
    {
        private IUserService sut;
        private IUserData userData;

        [SetUp]
        public void SetUp()
        {
            userData = MockRepository.GenerateStub<iuserdata>();
            sut = new UserService(userData);
        }

        [Test]
        public void UserServiceShouldCreateUserWithCorrectCreatedDate()
        {
            User user = null;

            // using Rhino Mocks to grab the User instance passed to the IUserData stub
            userData.Stub(x => x.SaveUser(null)).IgnoreArguments().Callback<user>(x =>
            {
                user = x;
                return true;
            });

            sut.CreateUser("mike");

            Assert.AreEqual(DateTime.UtcNow, user.CreatedDateTime);
        }
    }
But in this case, probably because Rhino Mocks is doing some pretty intensive proxying, a few milliseconds pass between the user being created and my assertions running.
Test 'Mike.Spikes.InjectingDateTime.UserServiceTests.UserServiceShouldCreateUserWithCorrectCreatedDate' failed: 
  Expected: 2015-05-28 09:08:18.824
  But was:  2015-05-28 09:08:18.819
 InjectingDateTime\InjectDateTimeDemo.cs(75,0): at Mike.Spikes.InjectingDateTime.UserServiceTests.UserServiceShouldCreateUserWithCorrectCreatedDate()
The solution is to inject a function that returns a DateTime:
    public class UserService : IUserService
    {
        private readonly IUserData userData;
        private readonly Func<datetime> now;

        public UserService(IUserData userData, Func<datetime> now)
        {
            this.userData = userData;
            this.now = now;
        }

        public void CreateUser(string username)
        {
            var user = new User(username, createdDateTime: now());
            userData.SaveUser(user);
        }
    }
Now our unit test can rely on a fixed DateTime value rather than one that is changing as the test runs:
    [TestFixture]
    public class UserServiceTests
    {
        private IUserService sut;
        private IUserData userData;

        // stub the system date as some arbirary date
        private readonly DateTime now = new DateTime(2015, 5, 28, 10, 46, 33);

        [SetUp]
        public void SetUp()
        {
            userData = MockRepository.GenerateStub<iuserdata>();
            sut = new UserService(userData, () => now);
        }

        [Test]
        public void UserServiceShouldCreateUserWithCorrectCreatedDate()
        {
            User user = null;
            userData.Stub(x => x.SaveUser(null)).IgnoreArguments().Callback<user>(x =>
            {
                user = x;
                return true;
            });

            sut.CreateUser("mike");

            Assert.AreEqual(now, user.CreatedDateTime);
        }
    }
And the test passes as expected.
In our composition root we inject the current system time (here as UTC):
    var userService = new UserService(userData, () => DateTime.UtcNow);
This pattern can be especially useful when we want to test business logic that relies on time passing. For example, say we want to check if an offer has expired; we can write unit tests for the case where the current (stubbed) time is both before and after the expiry time just by injecting different values into the system-under-test. Because we can stub the system time to be anything we want it to be, it makes it easy to test time based busines logic.