Fun With Testing DateUtils.pas #1
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.



Please write tests for the generic collection classes next…
March 10th, 2010 at 11:17 am+1 - go to generics
March 10th, 2010 at 11:50 amActually 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!
March 10th, 2010 at 12:10 pmDavid –
You make a good point — but my point is that I won’t alter the functionality of the product itself by writing a test.
March 10th, 2010 at 12:31 pmJarrod –
Perfect! Good feedback, I’ll get those fixed.
Nick
March 10th, 2010 at 12:38 pmNice 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,
March 10th, 2010 at 1:06 pmJarrod
Jarrod –
I went with:
case AMonth of
March 10th, 2010 at 2:43 pm2: 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;
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…)
March 10th, 2010 at 8:52 pm@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!!
March 10th, 2010 at 8:54 pmTry this one:
ADate := RecodeDateTime(Now, RecodeLeaveFieldAsIs, RecodeLeaveFieldAsIs, RecodeLeaveFieldAsIs, 24, 0, 0, 0);
This is logged as entry 82168 in Quality Central.
March 10th, 2010 at 10:05 pmThe 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.
March 11th, 2010 at 1:40 amThe 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,
March 11th, 2010 at 2:31 ambut 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.
A great start! Please go on
Can you give some info how you integrate DUnit in a CBS in a future blog?
March 11th, 2010 at 2:43 amHm. 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)
March 11th, 2010 at 5:31 amMatthias –
"a CBS?"
March 11th, 2010 at 6:03 amHallvard –
For which call?
March 11th, 2010 at 6:03 amNick, 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!
March 11th, 2010 at 7:30 amNick,
March 11th, 2010 at 9:43 amthere are some ugly DateUtils.pas bug in QC. Please consider these bugs too.
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.
March 11th, 2010 at 10:13 amNick:
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
March 11th, 2010 at 2:06 pmOh 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.
March 11th, 2010 at 9:09 pmMatthias –
We use Hudson to run all our tests and do our builds.
March 11th, 2010 at 9:14 pmWe 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.
March 11th, 2010 at 10:35 pmCan 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)
March 15th, 2010 at 11:20 pmI gotta learn it
I hope I’ll be able to pick up something.
April 12th, 2010 at 9:54 amNick, 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;

June 7th, 2010 at 2:51 amIt’s a pity that you didn’t do anything with issue 82168 in Quality Central.
June 17th, 2010 at 1:27 amIt’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?
Bert –
I wasn’t aware of that bug. It’s fixed:
http://docwiki.embarcadero.com/VCL/en/DateUtils.RecodeDateTime
June 17th, 2010 at 8:38 am