DUnit has several overloads of the
CheckEquals method, but we have our own testcase that descends from theirs and adds lots more overloads. Most of them are for enums. We have things like:
TMyTestCase = class(TTestCase)
procedure CheckEquals(Expected, Actual: TFormBorderStyle; Msg: string = ''); overload;
function EnumToStr(Value: TFormBorderStyle): string; overload;
procedure TMyTestCase.CheckEquals(Expected, Actual: TFormBorderStyle; Msg: string)
CheckEquals(EnumToStr(Expected), EnumToStr(Actual), Msg);
procedure TMyTestCase.EnumToStr(Value: TFormBorderStyle): string;
Result := GetEnumName(Ord(Value), TypeInfo(TFormBorderStyle));
Obviously, if you want other methods to support an enum too —
CheckNotEquals, for example — you have to overload them too. This really makes me wish I were working in a language with a unified type system.
When I started thinking about DUnitAssertions, enums were one of the first things I thought about, because you’d have to add overloaded methods in more than one place. Consider this assertion:
You’d have to add
TFormBorderStyle overloads to two methods to make this work —
Tis.EqualTo — instead of just having to overload
CheckEquals like in DUnit. Yuck.
After poking and prodding at this for a bit, I found what I think is a decent solution: I made a universal value type, which I’m calling
TValue. It’s a record type that supports
operator Implicit, so you can supply an integer, an Int64, a floating-point value, a Boolean, a string, an object, an interface, or any enum type it knows about. Then all the assertions just take
TValue parameters. So you can pass any type of value to those methods, without the methods themselves needing tons of overloads. The overloading is effectively moved from the methods onto
TValue, and centralized there. It’s actually kind of slick.
“But,” you may be thinking, “why a new type? What about
array of const? What about
Variant? Why make another universal value type, when Delphi already has at least two of them?”
Simple answer: because Delphi’s “universal” value types suck rocks when it comes to enums. With both
array of const and
Variant, if you pass in an enum value, they turn it into the ordinal value — so
bsDialog becomes 3. All type information is utterly lost, and if the assertion ever failed, you would get something extremely unhelpful like “Expected 0 but was 3”. In my view, that’s unacceptable. If you’re comparing enums, the assertion needs to say “Expected bsNone but was bsDialog”.
Having a custom-coded universal value type does fix that. It does introduce a few wrinkles of its own, though. For one, you have to write some code every time you want it to support a new enum. But that’s not a huge deal; you already had to do that to compare enums in DUnit. Actually, with the universal value type, you only need to write one method instead of two or more:
class operator TValue.Implicit(Value: TFormBorderStyle): TValue;
Result := TValue.Create(TEnumValue.Create(Ord(Value), TypeInfo(TFormBorderStyle)));
Once you’ve added the
operator Implicit for your enum, everything else will just work —
Tis.Not.EqualTo will just work with no extra effort. Even
Tis.GreaterThan would come along for the ride, if you were sadistic enough to want to do greater-than / less-than comparisons on enums.
A somewhat bigger issue is dependencies. With ordinary DUnit, you can write your own assertion methods that operate on enums, and put them in your own unit; there’s no need to edit
TestFramework.pas from the DUnit distribution. Not so with a universal value type that the test framework depends on. If you want to add a new enum to be supported, you’ll have to edit
Values.pas, which in turn is used by all the other DUnitAssertions units. So if you have a
TMyProprietaryEnum type in your
ProprietaryStuff unit, you’ll actually have to change the
Values unit in the DUnitAssertions distribution to depend on
This is distasteful, but remember that your test project already depends on
ProprietaryStuff. The only case where I can see genuine problems with making
Values.pas depend on
ProprietaryStuff.pas is if you have multiple different DUnit test EXEs, and you don’t want them all to depend on the same code base. In this case, you would have to have multiple separate copies of
Values.pas, one for each project, and set your search paths accordingly so that each project finds its own
TValue. Also distasteful, but it’s the best option Delphi’s type system leaves available.