Wednesday, May 13, 2009

Quick Tip: Consider Using 'AsOfDate' Instead of DateTime.Now

With LINQ-to-SQL, or even plain old SQL, it's tempting to assume the current date in a query or other method.  .NET lets us instantly know the date right this second with DateTime.Now, and TSQL with GetDate().  Using them in the wrong place, though, can introduce some subtle problems into code.  On the .NET side, code that assumes the current date is more difficult to test.  Your tests have to know that the code being tested assumes the current date.  Consider this example testing an extension method "GetExpiredItems"

items.Add(new Item{ ExpirationDate = DateTime.Now.AddDays(-4) });
items.Add(new Item{ ExpirationDate = DateTime.Now.AddDays(-3) });
items.Add(new Item{ ExpirationDate = DateTime.Now.AddDays(1) });
items.Add(new Item{ ExpirationDate = DateTime.Now.AddDays(2) });
var actual = items.GetExpiredItems();
Assert.AreEqual(2 , actual.Count())

Admitedly a poor example, but the point is that the more complex the method gets, the more difficult setting up the test becomes.  It also introduces the possibility that a test will pass for one date, but not another.  Rewriting it so that it doesn't assume today's date makes the test more specific:

items.Add(new Item{ ExpirationDate = DateTime.Parse("1/1/2000") });
items.Add(new Item{ ExpirationDate = DateTime.Parse("1/1/2000") });
items.Add(new Item{ ExpirationDate = DateTime.Parse("1/1/2011") });
items.Add(new Item{ ExpirationDate = DateTime.Parse("1/1/2010") });
var actual = items.GetExpiredItems(DateTime.Parse("1/1/2009"));
Assert.AreEqual(2 , actual.Count())

As a bonus, your code is more useful.  Your consumer can now not just get the current expired items, but also see expired items on future and past dates.  This is especially helpful when writing reports and similar in SQL.  How many times have you written a report or query assuming the current date, only to learn later that it needed to only show data for closed-out periods or some other arbitrary date?  Then you go and change all your GetDate()s.  This also allows for testing edge cases.  It recently helped us when a bug was discovered that only arose on leap days.

Obviously, DateTime.Now will need to be passed in eventually, but by allowing developers to pass the date in, that can be limited to a single place in the UI or business logic where it makes the most sense.  To continue the example above:

var actual = items.GetExpiredToday();

For reports and other code, it may simply mean letting the user choose a date, but defaulting to the current date. Anyway, this is a silly tip, but something we've run into a few times recently.  As a general rule of thumb, if you see DateTime.Now in your code, ask yourself it it would make more sense to pass it in further up the chain of calls.  This really is a specific example of a broader agile development practice, which is to defer decisions until the last responsible moment.  By putting off the decision of whether you really want to use today's date or another, you end up with more flexible, testable code.

No comments: