DUnitAssertions: Universal value type

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:

type
TMyTestCase = class(TTestCase)
strict protected
procedure CheckEquals(Expected, Actual: TFormBorderStyle; Msg: string = ''); overload;
function EnumToStr(Value: TFormBorderStyle): string; overload;
...
procedure TMyTestCase.CheckEquals(Expected, Actual: TFormBorderStyle; Msg: string)
begin
CheckEquals(EnumToStr(Expected), EnumToStr(Actual), Msg);
end;
procedure TMyTestCase.EnumToStr(Value: TFormBorderStyle): string;
begin
Result := GetEnumName(Ord(Value), TypeInfo(TFormBorderStyle));
end;

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:

Expect.That(Form.BorderStyle, Tis.EqualTo(bsNone));

You’d have to add TFormBorderStyle overloads to two methods to make this work — Expect.That and 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;
begin
Result := TValue.Create(TEnumValue.Create(Ord(Value), TypeInfo(TFormBorderStyle)));
end;

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 ProprietaryStuff.pas.

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.

Leave a Reply

Your email address will not be published. Required fields are marked *