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.
with FluentAssertions you can write
ReplyDeleteinsertDate.Should().BeCloseTo(DateTime.Now)
I believe that Noda Time has an interface that can be injected also if you don't mind adding another dependency to get hold of it. Probably gives more flexibility because you are injecting a single function which is already determined to be UTC time. In some scenarios you may need both and it would seem to be a bit painful to inject both a UTC version of the function and a local time version.
ReplyDeleteSomeone where I work created a static TimeProvider class to try and solve the same problem, which was a truly terrible idea, as tests running concurrently stomp on each others expected time..
ReplyDeleteGood post- another option option which you haven't mentioned is using something like Microsoft Fakes.
ReplyDeleteInteresting solution. I didn't think of doing this with functions.
ReplyDeleteMy solution to this problem (and that includes other things like generating Guids and other non-deterministic things) is to create an interface called IEnvironment, the default implementation goes off to DateTime.UtcNow, Guid.NewGuid(), etc. In unit tests I can then mock that out. It also means that if I have to do a couple of things, I only have one extra thing to add to the constructor.
I like the idea of an IEnvironment facade. Especially for faking Guids. A simple TestDataBuilder can then provide doubles to spec. Thank you Colin. Something to mull over next week :)
DeleteI prefer in this case create TimeProvider.
ReplyDeleteUsing this Fake implementation we can go back and forward in time.
Somethin like this.
public interface ITimeProvider
{
DateTime UtcNow();
}
public class TimeProvider : ITimeProvider
{
public DateTime UtcNow()
{
return DateTime.UtcNow;
}
}
public class FakeTimeProvider : ITimeProvider
{
private DateTime _dateTime;
public FakeTimeProvider(DateTime dateTime)
{
_dateTime = dateTime;
}
public DateTime UtcNow()
{
return _dateTime;
}
public void SetTime(DateTime newDateTime)
{
_dateTime = newDateTime;
}
}
public class UserService : IUserService
{
private readonly ITimeProvider _timeProvider;
public UserService(IUserData userData, ITimeProvider timeProvider);
}
[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();
sut = new UserService(userData, new FakeTimeProvider(now));
}
[Test]
public void UserServiceShouldCreateUserWithCorrectCreatedDate()
{
User user = null;
userData.Stub(x => x.SaveUser(null)).IgnoreArguments().Callback(x =>
{
user = x;
return true;
});
sut.CreateUser("mike");
Assert.AreEqual(now, user.CreatedDateTime);
}
}
I like this idea a lot Mike. I prefer to use a custom delegate too Func
ReplyDeletepublic delegate Now();
I think it reveals intent and is simpler to use with an IoC container.
I'm not keen on interfaces to provide date time, I think it's over engineering. All that's required is a substitutable function, anything else is bloat if all you need is DateTime.
Sometimes I will create a type to wrap some time functions. For example. I don't like to serialise / deserialise a complex type like DateTime, especially in JSON. I convert to a value representing seconds or milliseconds since an epoch. It removes ambiguity and complexity. Extenuated if your serialising / deserialising cross platform i.e. Windows C# <-> Node Linux. Even my wrapper type will take the Now delegate. The only DateTime.Now() in my code is in the application bootstrapping when it's assigned to the Now delegate.
This comment has been removed by the author.
ReplyDeleteA much less ridiculous solution:
ReplyDeletepublic void CreateUser(string username, DateTime? time = null) {
if(time == null) time = DateTime.UtcNow;
var user = new User(username, createdDateTime: time);
userData.SaveUser(user);
Of course you shouldn't be taking audit data like created time inside an object constructor. I'll let that one slide that it's a contrived example and not a serious example.
Ambient Context, anyone?
ReplyDeletehttp://blogs.msdn.com/b/ploeh/archive/2007/07/23/ambientcontext.aspx