Nick Hodges

Fun With Testing DateUtils.pas #4

01 Apr

First, an admin note:  I’ve adjusted the color of strings in my code.   I was optimizing the colors for reading on my blog proper as opposed to the main site (hadn’t even thought of it, actually, sorry.), and someone pointed out that the colors weren’t working on the main site at all.  Hope that this post is better.  I changed the last post from Yellow to Lime.  If you have a better color suggestion, please let me know.  I’ve also endeavored to wrap those long code lines. The code won’t compile as shown, but I trust that you guys can figure it out……

Okay back to the topic at hand.

So things are rolling along.  I’ve been writing tons of tests, they are all passing, things are going well, and it’s been fun.

But if you have any flair for the dramatic, you can see where this is going….

So there I was rolling along, writing tests for WeeksInAYear (bet you didn’t know that according to ISO 8601, some years have 53 weeks in them, did you.  1981 has 53 weeks, for example) Today, Yesterday – you know, normal stuff.  I’m checking edge conditions, standard conditions, all kinds of years, every year.  You know, really exercising things.  All was rolling along smoothly.

For instance, here are the tests for Yesterday.  Not too hard to test, as there is really only one thing you can do:


procedure TDateUtilsTests.Test_Yesterday;
var
  TestResult: TDateTime;
  Expected  : TDateTime;
begin
  TestResult := Yesterday;
  Expected   := IncDay(DateOf(Now), -1);
  CheckEquals(TestResult, Expected, 'The Yesterday function failed to
    return the correct value.');

  TestResult := Yesterday;
  Expected   := DateOf(Now);
  CheckFalse(SameDate(TestResult, Expected), 'The Yesterday function thinks
    Yesterday is Today, and means that Einstein was totally wrong.');

end;

Just a couple of tests that you can do – or at least what I can think of.  (Anyone have any other ideas?)  The fun part is that these tests will fail if IncDay and DateOf fail to perform as advertised, we get triple the testing!  Sweet!

Things were going along swimmingly, and then all of a sudden, out of left field, all this unit testing stuff suddenly proved to be as valuable as everyone says it is.

Here’s how it happened: I was going along, writing tests, and I wrote this one:


procedure TDateUtilsTests.Test_EndOfTheDay;
var
  TestDate  : TDateTime;
  TestResult: TDateTime;
  i         : Integer;
  Expected  : TDateTime;
begin
  for i        := 1 to 500 do
  begin
    TestDate   := CreateRandomDate(False, 100, 2500);

    TestResult := EndOfTheDay(TestDate);
    // First, don't change the date
    CheckEquals(DayOf(TestDate), DayOf(TestResult), Format('EndOfTheDay changed
      the day for test date: %s (Result was: %s)', [DateTimeToStr(TestDate),
      DateTimeToStr(TestResult)]));

    // Next, is it really midnight?
    Expected := DateOf(TestDate);
    Expected := IncMillisecond(Expected, -1);
    Expected := IncDay(Expected);
    CheckTrue(SameDateTime(TestResult, Expected), Format('EndOfTheDay didn''t
      return midnight for test date: %s (Result was: %s, Expected was: %s)',
      [DateTimeToStr(DateOf(TestDate)), DateTimeToStr(TestResult),
       DateTimeToStr(Expected)]));

  end;
end;

 

Pretty simple and straightforward.  But — BOOM – this thing fails. Badly.  If you run this test on your computer, the second check, the call to CheckTrue, will pretty quickly fail and you’ll get a message something like:

Test_StartEndOfTheDay: ETestFailure at  $0051FF06 EndOfTheDay didn’t return midnight for test date: 5/12/0366 (Result was: 5/12/0366 11:59:59 PM, Expected was: 5/14/0366 11:59:59 PM), expected: <True> but was: <False>

Since the test is creating random dates, you’ll never get the exact same error, but pretty soon I figured out that it only failed for dates before the epoch – that is, for dates that have a negative value and are thus earlier than 30 December 1899. 

Naturally, I was left scratching my head.  The first inclination is that the test is somehow not correct. But I stared at it for a good long while and came to the conclusion that the test wasn’t the problem. 

The first check is fine – the call to EndOfTheDay doesn’t actually change the date as it shouldn’t.  But the second test is where the trouble started. 

EndOfTheDay is a pretty simple function;  it returns the very last millisecond of the date for the date/time combination passed to it – that is, 11:59.999pm for the day in question. It is implemented like so:


// From DateUtils.pas
function EndOfTheDay(const AValue: TDateTime): TDateTime;
begin
  Result := RecodeTime(AValue, 23, 59, 59, 999);
end;

So the natural thing is to actually check to see if the result is indeed that value.  So, I did the natural thing:  I set the expected date to midnight on the date of the value to be tested, decremented one millisecond, and since that changed the date back one day, I moved it forward again with IncDay.  Then I checked to see if they were indeed the same date/time combination.  Well, guess what.  They weren’t. 

I originally had a single line of code combining the three that set the value for Expected.  A quick look at the debugger told me that the Expected result wasn’t getting properly calculated.  Breaking it down quickly pointed to a strange phenomenon:  for dates before the epoch, the IncMillisecond call was actually moving the date portion forward  by two days if the date was before the epoch.  (Mysteriously, dates after epoch all worked fine.  Weird.)  That, of course, is a big bad bug. 

And this is the part where using the library itself to test other parts of the library is helpful.  Because I used IncMillisecond in my test for EndOfTheDay, I found a bug in IncMillisecond. If I hadn’t done so, the problem might have been left lurking for a while longer.  Or maybe it never would have revealed itself, depending on how diligent my testing of it ended up once I actually got there. 

Luckily, it would appear that not too many of you are manipulating milliseconds for dates before the epoch, because there hasn’t been a big hue and cry about this problem. There have been some QC reports about it, though.  But clearly something is dreadfully wrong here. 

In the next post, we’ll take a look at just what that is.

10 Responses to “Fun With Testing DateUtils.pas #4”

  1. 1
    Twitter Trackbacks for Nick Hodges » Blog Archive » Fun With Testing DateUtils.pas #4 [embarcadero.com] on Topsy.com Says:

    [...] Nick Hodges » Blog Archive » Fun With Testing DateUtils.pas #4 blogs.embarcadero.com/nickhodges/2010/04/01/39379 – view page – cached First, an admin note: I’ve adjusted the color of strings in my code. I was optimizing the colors for reading on my blog proper as opposed to the main site (hadn’t even thought of it, actually, sorry.), and someone pointed out that the colors weren’t working on the main site at all. Hope that this post is better. I changed the last post from Yellow to Lime. If you have a better color… Read moreFirst, an admin note: I’ve adjusted the color of strings in my code. I was optimizing the colors for reading on my blog proper as opposed to the main site (hadn’t even thought of it, actually, sorry.), and someone pointed out that the colors weren’t working on the main site at all. Hope that this post is better. I changed the last post from Yellow to Lime. If you have a better color suggestion, please let me know. I’ve also endeavored to wrap those long code lines. The code won’t compile as shown, but I trust that you guys can figure it out…… View page Filter tweets [...]

  2. 2
    Mike Says:

    Yes some years have 53 weeks and there is a difference between US and Europe how to calculate the first week of year and some regions start the week on sunday … - this makes sense as then monday is the second day of the the week and is not soooo hard to survive.

  3. 3
    Bruce McGee Says:

    I like these posts.

    Two points:

    1) I would include specific dates and date ranges instead of random values. Otherwise, your tests aren’t repeatable and may pass "by accident".

    2) It’s great that the tests for EndOfTheDay also exercise at least some of the functionality in other methods, but once you found an issue with IncMillisecond, I hope you added a test suite specifically for this method, as well.

    I would avoid randomness

  4. 4
    Nick Hodges Says:

    Bruce –

    I should have mentioned that in the code above, I had cut out some of the other tests. All of my tests include static tests. I only use Random data after thoroughly testing with static data.

  5. 5
    Bruce McGee Says:

    Nick,

    Fair enough. And presumably, if the random testing turns up something not already in the static tests, you add a new static test for that specific case.

  6. 6
    M J Marshall Says:

    So, are any of these bug-fixes going to released for D2010? Or are we going to have to wait for D2011?

  7. 7
    Jarrod Hollingworth Says:

    It’s great to see one topic get so much coverage and it shows some of the subtelties of unit testing.

    I’ve nit-picked a couple of the other posts and have the following as well so please take it as constructive criticism.

    Whenever you rely on comparing two "current date times" in a unit test (or any code) you need to be consider that there is an interval between them.

    For example the following code will work most of the time but fail on rare occasions when crossing midnight (in an continuous build/integration environment or if you work late!):

    TestResult := Yesterday;
    Expected := IncDay(DateOf(Now), -1);

    It may not be worth coding around this midnight exception though. A possible solution is to get Now prior to and after Yesterday and comparing the two Now’s to check for a date change and change the value expected.

  8. 8
    Nick Hodges Says:

    Jarrod –

    That test that you have above should always pass if you compare /dates/ but not /datetimes/, with the rare exception that midnight passes at the moment between the two calls.

    Is that what you are referring to?

  9. 9
    Jarrod Hollingworth Says:

    It will fail if crossing midnight between the two lines of code shown, regardless of whether you test dates or date-times. At 23:59:59.999 Yesterday might be say 13-Apr-2010 then at 00:00:00.001 IncDay(DateOf(Now), -1) would be 14-Apr-2010.

  10. 10
    Jim McKeeth Says:

    One more note about using random testing: Report the random seed that was used to generate the random numbers on a failure. If you only report the data when the test failed then you are no accounting for any state that may have occured leading to the failure. While if you report the random seed then you can create a test that reproduces the exact sequence of data that led to the failure.

    True you generally try to issolate the state, but it does happen from time to time. You only need the seed when the data that failed does not reproduce the failure, but when you do need it you will be glad you have it!

© 2014 Nick Hodges | Entries (RSS) and Comments (RSS)

Your Index Web Directorywordpress logo
Close