Nick Hodges

Fun With Testing DateUtils.pas #1

10 Mar

Okay, so I’m a Development Manager. My job is to see to the health, welfare, productivity, effectiveness, and proper tasking of a big chunk of the RAD Studio development team. I share these duties with the excellent and capable Mike Devery.  I mainly manage the guys that work on the IDE, the RTL, and the frameworks.  I do things like make sure they are working on the right thing via our SCRUM interations, that they have good machines, nice chairs, vacations when they want them, the right keyboard, etc.  I manage the development process in that we on the “War Team” spend a lot of time triaging bugs, managing and defining requirements, tracking progress, finding better ways to write better code – you know, development manager stuff.

But, like all good development managers, my heart really is in coding.  And I don’t do much of that anymore.  So I’ve tried to keep my fingers in things by taking on some small development tasks where I can keep my skills up, stimulate my brain in that way, and not cause too much damage. 

What better way to do that than to write unit tests?  I’m a bit weird in that I actually like to write unit tests. I find them challenging and enjoy the “puzzle solving” aspect of it.  I like to try to find corner and edge cases where the tests might fail.  I like knowing that ever test I write means less work down the road because any regressions will be found sooner – hopefully immediately.  And I can write tests to my hearts content without worrying about breaking the product.  ;-)

So I started in on DateUtils.pas.  This is a pretty cool unit with a lot of good functionality, and it’s ripe for expanding on our unit tests.  It was written a while ago, and its unit test coverage wasn’t where it should be.  So in my “spare time” (hehe….) I’ve been writing unit tests for the routines in that unit.  It’s been pretty fun, and I think, too, that it’s been illustrative of how beneficial unit testing can be.  So I thought I’d write a series of blog posts about it, and this is the first one. 

So, one of the first things I realized was that I needed to be able to generate dates.  Now, I realized that you don’t want that many non-deterministic tests (or maybe you don’t want any at all – it depends).  But I need to be sure that many of the DateUtils.pas routines can pass with any date.  So I wrote the following routine to generate a legitimate but random date:


/// <summary>
///   This creates a random, valid date from year 1 to aYearRange
/// </summary>
function CreateRandomDate(aMakeItLeapYear: Boolean = False; aYearRange: Word = 2500): TDateTime;
var
  AYear, AMonth, ADay, AHour, AMinute, ASecond, AMilliSecond: Word;
begin
  AYear := Random(aYearRange) + 1;
  if (not IsLeapYear(AYear)) and (aMakeItLeapYear) then
  begin
    repeat
      Inc(AYear);
    until IsLeapYear(AYear);
  end;
  AMonth := Random(MonthsPerYear) + 1;
  if IsLeapYear(AYear) and (AMonth = 2) then
  begin
    ADay := Random(29) + 1;
  end else begin
    ADay := Random(28) + 1;
  end;
  AHour := Random(HoursPerDay) - 1;
  AMinute := Random(MinsPerHour) - 1;
  ASecond := Random(SecsPerMin) - 1;
  AMilliSecond := Random(MSecsPerSec);
  Result := EncodeDateTime(AYear, AMonth, ADay, AHour, AMinute, ASecond, AMilliSecond);
end;

Now I’ll bet that you guys can come up with a better algorithm, but this works just fine for testing purposes and is pretty clear in what it does.  I use it to test, say, 1000 random dates against a routine that takes a Date as a parameter.  (By the way, it uses some constants from SysUtils and from DateUtils.)  I mix that in with tests that used constant dates every time.  I debated whether to do the random date thing (if a test fails, you can’t necessarily reproduce it), but I decided in favor of it because I’ll make sure all the tests clearly report the date that was failing, and because I wanted to test to make sure that any date would be handled correctly, and you simply can’t do that with a limited, fixed set of dates.  Constantly running a large set of random dates is as close as you can come to testing “every” date.

Another thing that I knew I’d need was to generate to do date testing is valid “Leap Days’”, that is, a valid February 29 date.  When you unit test, you are constantly looking for corner cases, and Leap Days are a corner case for dates.  Naturally, I’ll utilize CreateRandomDate to help out:


function GetRandomLeapDay: TDate;
begin
  Result := DateOf(CreateRandomDate(True));
  Result := RecodeDate(Result, YearOf(Result), 2, 29);
end;

So, now I can generate random dates, and easily create corner case LeapDays. 

I’ll probably also eventually add a GenerateRandomTime routine as well. 

That’s it for now – next time I’ll talk about the basics of how I got tests up and running. By the way, I am using DUnit.  We use DUnit extensively internally.

29 Responses to “Fun With Testing DateUtils.pas #1”

  1. 1
    Jeremy North Says:

    Please write tests for the generic collection classes next…

  2. 2
    Ronaldo Says:

    +1 - go to generics

  3. 3
    David Heffernan Says:

    Actually it’s quite easy to do harm to your product by writing unit tests. If the tests aren’t good enough then you can engender a false sense of security. And too many tests (e.g. many similar tests when one would provide the same coverage) is bad too because it’s harder then to judge how well you are covered by the tests.

    So, in my view you have to view the tests as being an integral part of the product.

    Note that I’m not saying that you guys don’t, or that your tests are poor. I’m just commenting on the view that writing tests can’t harm a product - I disagree!

  4. 4
    Nick Hodges Says:

    David –

    You make a good point — but my point is that I won’t alter the functionality of the product itself by writing a test.

  5. 5
    Nick Hodges Says:

    Jarrod –

    Perfect! Good feedback, I’ll get those fixed.

    Nick

  6. 6
    Jarrod Says:

    Nice to get your hands dirty!

    There are a few holes in CreateRandomDate though:

    - Don’t subtract 1 from the Hour/Min/Sec. The current function generates invalid times.
    - It never generates 30th or 31st of the month. Test for AMonth = 2 and then do either 28/29 depending on leap year, else if AMonth in [4,6,9,11] use 30, else 31.
    - It relies on IsLeapYear and EncodeDateTime which could be themselves the test subject, so you’d want to also test them using some hard-coded dates likely to trip them up.

    Also you might want to allow a specific minimum year to allow for intensive checking around known target years (such as 2000 leap year and 2100 non leap year). ie:

    aYearRangeLow: Word = 1; aYearRangeHigh: Word = 2500
    AYear := Random(aYearRangeHigh - aYearRangeLow + 1) + aYearRangeLow;

    Cheers,
    Jarrod

  7. 7
    Nick Hodges Says:

    Jarrod –

    I went with:

    case AMonth of
    2: begin
    if IsLeapYear(AYear) then
    begin
    ADay := Random(29) + 1;
    end else
    begin
    ADay := Random(28) + 1;
    end;
    end;
    1, 3, 5, 7, 8, 10, 12: begin
    ADay := Random(31) + 1;
    end;
    4, 6, 9, 11: begin
    ADay := Random(30) + 1;
    end;
    end;

  8. 8
    Anders Isaksson Says:

    The code as shown in the blog is not setting the ADay variable for any other month than February. It seems the comments fix that, but don’t trust people to read all the comments before commenting :-)

    OT: I don’t know about you, but for me, in the formatted code, the blue keywords on the (almost) black background is *completely*, *utterly* unreadable, the white on black is too high contrast, and the green comments are perfect (good contrast, still low intensity).

    (I think I have mentioned this before…)

  9. 9
    David Heffernan Says:

    @Nick I know perfectly well what your point is. I’m just saying that the product is more than the code that ships. Actually I think this is a really important realisation and I’ll be discussing it with my team at our stand up meeting today. Thanks for prompting these thoughts!!

  10. 10
    Bert Says:

    Try this one:

    ADate := RecodeDateTime(Now, RecodeLeaveFieldAsIs, RecodeLeaveFieldAsIs, RecodeLeaveFieldAsIs, 24, 0, 0, 0);

    This is logged as entry 82168 in Quality Central.

  11. 11
    Patrick van Logchem Says:

    The range of arguments passed to SysUtils.TryEncodeDate seem to indicate valid values are from 1/1/1 to 31/12/9999, which covers 3652058 days, which boils down to a range of [-693593..2958465] (the negative start is because DateDelta uses a value of 693595, to make value 1 mean 1/1/1900 instead of 1/1/1, which was to make TDateTime backwards-compatible to Delphi 1, no less!)

    So to get a random date, you could just do this:

    function GetRandomDate: TDate;
    const
    MaxDateRange = 3652058; // 365 * 9999 plus a bunch of leap days
    begin
    Result := Random(MaxDateRange) - DateDelta;
    end;

    And for getting a LeadyDay in a random year, this is simpler :

    function GetRandomLeapDay: TDate;
    begin
    Result := EncodeDate(Result, Random(9999)+1, 2, 29);
    end;

    Cheers!

    PS: There’s a bit of fuzzy date-mangling going on regarding the base of the Gregorian calendar, but we’d better ignore that for now, won’t we? ;-) See http://hcal.ccarh.org/hcal.html and http://en.wikipedia.org/wiki/Gregorian_calendar for more info.

  12. 12
    Patrick van Logchem Says:

    The range of arguments passed to SysUtils.TryEncodeDate seem to indicate valid values are from 1/1/1 to 31/12/9999,
    which covers 3652058 days, which boils down to a range of [-693593..2958465]
    (the negative start is because DateDelta uses a value of 693595, to make value 1 mean 1/1/1900 instead of 1/1/1,
    which was to make TDateTime backwards-compatible to Delphi 1, no less!)

    So to get a random date, you could just do this:

    function GetRandomDate: TDate;
    const
    MaxDateRange = 3652058; // 365 * 9999 plus a bunch of leap days
    begin
    Result := Random(MaxDateRange) - DateDelta;
    end;

    And for getting a LeadyDay in a random year, this is simpler :

    function GetRandomLeapDay: TDate;
    begin
    Result := EncodeDate(Result, Random(9999)+1, 2, 29);
    end;

    Cheers!

    PS: There’s a bit of fuzzy date-mangling going on regarding the base of the Gregorian calendar,
    but we’d better ignore that for now, won’t we? ;-)
    See http://hcal.ccarh.org/hcal.html and http://en.wikipedia.org/wiki/Gregorian_calendar for more info.

  13. 13
    Matthias Says:

    A great start! Please go on

    Can you give some info how you integrate DUnit in a CBS in a future blog?

  14. 14
    Hallvard Vassbotn Says:

    Hm. Since a Julian date is just an integer number it would be much simpler just to say:

    Result := Random(DateRange);

    No? ;)

    (Disregarding the aMakeItLeapYear parameter)

  15. 15
    Nick Hodges Says:

    Matthias –

    "a CBS?"

  16. 16
    Nick Hodges Says:

    Hallvard –

    For which call?

  17. 17
    M J Marshall Says:

    Nick, actually writing test cases CAN damage a product: An incorrectly coded test can create a false failure, which then results in product changes to "fix" the non-existent problem, thus damaging the product. Similarly, that same incorrect test can create a false success, allowing a bug to pass undetected into the shipping product.

    One could argue that test cases need to be developed even more carefully than the code to be tested!

  18. 18
    Peter Says:

    Nick,
    there are some ugly DateUtils.pas bug in QC. Please consider these bugs too.

  19. 19
    Jorge Says:

    Add this one to your test:
    ShortDateFormat := ‘yyyy-mmm-dd’;
    dt := StrToDate(DateToStr(now));

    I know its not an issue in DateUtils, but the user can enter damn near anything for a date time preference in control panel. MS tools handle this gracefully.

  20. 20
    Mark Says:

    Nick:

    Great post.

    One question: How do I find an IP address on the network?

    There is function in Delphi for that?

    I would check for address "192.168.0.111: \\Data\HData.gdb", and have received false or true.

    Thanks,

    Mark

  21. 21
    Matthias Says:

    Oh Sorry, CBS = ‘Continous Build System’ or some are more familiar to ‘continous integration’.

    We use Hudson (https://hudson.dev.java.net/) for it in our Delphi environment.

  22. 22
    Nick Hodges Says:

    Matthias –

    We use Hudson to run all our tests and do our builds.

  23. 23
    Matthias Says:

    We used DUnit2 for the unit tests and I implemented a JUnit compatible listener. Hudson is now able to track and display all results.

    Did you do something similar?

    BTW: Another listener in co-op of an IDE expert made unit testing in Delphi much more fun ;-) Double-click on unit test failure messages in the IDE output window brings up the test code line.

  24. 24
    Ken Knopfli Says:

    Can one unit test the IDE, too?

    I ask because on my PC with two monitors, if you save your own default layout, it does not restore correctly - it all squashes everything to the first screen.

    Oddly, if you then manually swap to some other layout, then to the default again, it is correct!

    It only fails when starting Delphi 2010 - which is when you need it, actually… (Note: it works in Delphi 5)

  25. 25
    Karpacz Says:

    I gotta learn it :D I hope I’ll be able to pick up something.

  26. 26
    Hallvard Vassbotn Says:

    Nick, I just meant simplifying the function to:

    function CreateRandomDate(aYearRange: Word = 2500): TDateTime;
    var
    ADateRange: integer;
    begin
    ADateRange := aYearRange * 365; // More or less :)
    Result := Random(ADateRange);
    end;

    :)

  27. 27
    Bert Says:

    It’s a pity that you didn’t do anything with issue 82168 in Quality Central.
    It’s either a documentation bug that has been addressed in help update 3 or it should be openend for fixing.
    Why are you blogging about DateUtils if you don’t do anything with feedback?

  28. 28
    Nick Hodges Says:

    Bert –

    I wasn’t aware of that bug. It’s fixed:

    http://docwiki.embarcadero.com/VCL/en/DateUtils.RecodeDateTime

  29. 29
    Continuous Integration mit Hudson – Hassmann-Software Says:

    [...] http://blogs.embarcadero.com/nickhodges/2010/03/10/39369 [...]

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

Your Index Web Directorywordpress logo
Close