So you created a generic class. Maybe a specialized generic collection, like a generic set.
Professional as you are, you are going to need some unit tests for this class. But how do you test a class that can have so many variations depending on the type argument. You could write test cases for just one or two type arguments (for example to test TgoSet<Integer>
and TgoSet<String>
) and assume the class works with other types as well.
But what you actually want is to create a generic unit test, that can test a wide variety of types. Including apples and oranges.
A generic test case class
Assuming you are using DUnitX for your units tests, you could make a generic base class for your test cases:
type TTestCollectionBase<T> = class abstract ... end;
If you are using DUnit instead of DUnitX, everything in this post still applies. Just derive the base class from
TTestCase
.
Then you derive your test class from this base class, still as a generic class:
type TTestTgoSet<T> = class(TTestCollectionBase<T>) private FCUT: TgoSet; public [Setup] procedure SetUp; [Teardown] procedure TearDown; [Test] procedure TestAdd; ... end; procedure TTestTgoSet<T>.SetUp; begin inherited; FCUT := TgoSet.Create; end; procedure TTestTgoSet<T>.TearDown; begin inherited; FCUT.Free; end;
We create an instance of our “Class Under Test” (FCUT
) in the SetUp
method, and release it again in the TearDown
method. Pretty boilerplate.
Then we can register a whole bunch of variations (instantiated types) of the test class with DUnitX:
initialization TDUnitX.RegisterTestFixture(TTestTgoSet<Integer>); TDUnitX.RegisterTestFixture(TTestTgoSet<Boolean>); TDUnitX.RegisterTestFixture(TTestTgoSet<Double>); TDUnitX.RegisterTestFixture(TTestTgoSet<TFoo>); TDUnitX.RegisterTestFixture(TTestTgoSet<IBaz>); TDUnitX.RegisterTestFixture(TTestTgoSet<String>); TDUnitX.RegisterTestFixture(TTestTgoSet<Int64>); TDUnitX.RegisterTestFixture(TTestTgoSet<TBytes>); TDUnitX.RegisterTestFixture(TTestTgoSet<TTestArray>); TDUnitX.RegisterTestFixture(TTestTgoSet<TTestRecord>); ...etc...
Testing different variations
You still need to test the different types though, and you want to do that in a generic way. Say, you want to test the TgoSet<T>.Add
method. If T
is an integral type, you could test it like this:
procedure TTestTgoSet<T>.TestAdd; begin FCUT.Add(3); FCUT.Add(42); Assert.IsTrue(FCUT.Contains(3)); Assert.IsTrue(FCUT.Contains(42)); Assert.IsFalse(FCUT.Contains(123)); end;
Of course, this will not compile, because <T>
does not have to be an integral type. You cannot add number 42
to the set if it is a set of strings. You will get the compilation error “Incompatible types: ‘T’ and ‘Integer'”.
We need a generic way to create a value that we can add to the set (or any generic class we want to test). So let’s do just that: add a CreateValue
method to our base class that returns a value of the target type:
type TTestCollectionBase<T> = class abstract protected function CreateValue: T; end;
Of course, we still need to fill this value with some data, so we need to give the method some data to use to create the value. We should pick a type for this data that can easily be converted to other types. The simple Integer
type comes to mind, since we can easily convert this to floating-point values, enums, strings or even classes, interfaces and records if we give those an Integer
-type property. So the actual declaration becomes this:
type TTestCollectionBase<T> = class abstract protected function CreateValue(const AValue: Integer): T; end;
Creating generic values
But what would the implementation of this method look like? It would have to do something like this:
- If
<T>
is an integral type, then just returnAValue
. - If
<T>
is a string type, then returnIntToStr(AValue)
. - If
<T>
s aTFoo
class, then create an instance of theTFoo
class, set itsValue
property toAValue
and return the instance. - Etc…
We can use the “compiler magic” function GetTypeKind
to check the type and act accordingly:
function TTestCollectionBase<T>.CreateValue(const AValue: Integer): T; begin case GetTypeKind(T) of tkInteger: Result := AValue; tkUString: Result := IntToStr(AValue); ... end; end;
Of course, this also does not compile because you cannot set Result
to an integer value since T
is not necessarily an integer. You may wonder if you could fix this with a type cast, like: Result := T(AValue)
. But that also doesn’t work and you will get an “Invalid typecast” compile error.
You can fix this with a type cast, but you have to resort to some ugly pointer juggling:
function TTestCollectionBase<T>.CreateValue(const AValue: Integer): T; begin case GetTypeKind(T) of tkInteger: PInteger(@Result)^ := AValue; tkUString: PString(@Result)^ := IntToStr(AValue); ... end; end;
This takes the address of the return value, casts it to a pointer of the target type, and then deferences to pointer to set its value. Not only is this ugly, it can also be dangerous, especially when using pointers to managed types such as strings, interfaces, dynamic arrays or even PODO’s on ARC platforms.
This is a case where the obscure absolute
keyword can come to the rescue.
Absolute variables
You may never have used absolute
before or don’t know what it is or does. The absolute
keyword allows you to overlay variables on top of each other; a bit like variant parts in records.
Normally, when you declare some variables, they may be laid out in a linear fashion in memory like this:
var I: Integer; D: Double; S: String;
With the absolute
keyword, you can put these 3 variables at the same memory address, which resembles more of a stacked layout:
var I: Integer; D: Double absolute I; S: String absolute I;
Now, the D
and S
variables occupy the same memory space as I
. This can be dangerous. For example, if you modify the value of I
, you are also modifying the address of the string S
to something that is probably invalid. If you would use S
later, bad things will happen…
But sometimes absolute variables can be useful. For example, by overlaying a 32-bit integer on top of a 32-bit floating-point number, you can get access to the individual bits of a floating-point value. With some knowledge of the IEEE Single-precision floating-point format, we can then extract the sign bit, exponent and fraction. As a quick detour, this would look like this:
var S: Single; C: Cardinal absolute S; Exponent: Integer; Fraction, Recon: Double; begin S := 12.34; { Extract exponent and fraction using bit masks } Exponent := ((C shr 23) and $FF) - 127; Fraction := 1 + ((C and $007FFFFF) / (1 shl 23)); { Reconstruct original value using fraction and exponent } Recon := Fraction * Power(2, Exponent); end;
Nowadays, with Delphi’s record helpers, we can just use code like Exponent := S.Exponent
instead.
Note that we can achieve the same result by using a variant record, as in:
type TSingleBits = record case Byte of 0: (S: Single); 1: (C: Cardinal); end;But you cannot use managed types (like strings) in variant parts of records.
Using the absolute
keyword, we can rewrite the CreateValue
method to:
function TTestCollectionBase<T>.CreateValue(const AValue: Integer): T; var IntValue: Integer absolute Result; StrValue: String absolute Result; ... begin case GetTypeKind(T) of tkInteger: IntValue := AValue; tkUString: StrValue := IntToStr(AValue); ... end; end;
This looks a lot cleaner. It is pretty safe too because the compiler will only use those absolute variables that match type T
(bacause of the case
statement). You just have to be careful to use the correct variable, but don’t you always…
Using type details
Up till now, we assumed that type kind tkInteger
is used for 32-bit integers only, but that is not always the case. It is also used to represent 8-bit and 16-bit integers (but not 64-bit integers though). So we need more details about the type. We could use the System.Rtti
unit to retrieve type details, but in this case using the GetTypeData
function from the System.TypInfo
unit is sufficient. It returns a pointer to a record of type TTypeData
that contains all sorts of details about a type. For integer types, we can use its OrdType
field which tells us what kind of integer it is.
So a better version of CreateValue
would look like this:
function TTestCollectionBase<T>.CreateValue(const AValue: Integer): T; var TypeData: PTypeData; I8: Int8 absolute Result; U8: UInt8 absolute Result; I16: Int16 absolute Result; U16: UInt16 absolute Result; I32: Int32 absolute Result; U32: UInt32 absolute Result; ... begin case GetTypeKind(T) of tkInteger: begin TypeData := GetTypeData(TypeInfo(T)); case TypeData.OrdType of otSByte: I8 := AValue; otUByte: U8 := AValue; otSWord: I16 := AValue; otUWord: U16 := AValue; otSLong: I32 := AValue; otULong: U32 := AValue; else System.Assert(False, 'Invalid integer type'); end; end; ... end; end;
Testing different variations – revisited
With this generic CreateValue
method, we can rewrite our test method to something like this:
procedure TTestTgoSet<T>.TestAdd; var V1, V2, V3: T; begin V1 := CreateValue(3); V2 := CreateValue(42); V3 := CreateValue(123); FCUT.Add(V1); FCUT.Add(V2); Assert.IsTrue(FCUT.Contains(V1)); Assert.IsTrue(FCUT.Contains(V2)); Assert.IsFalse(FCUT.Contains(V3)); end;
This will work with a lot of types now, not just integer types.
Testing custom types
We have focused on built-in types like Integer
and String
so far. But what about classes, records, interfaces and other custom types?
Updating the CreateValue
method to support these types isn’t too complicated. For example, say we want to test a set of TFoo
objects, where TFoo
looks like this:
type TFoo = class private FValue: Integer; public property Value: Integer read FValue; end;
The CreateValue
would be extended like this:
function TTestCollectionBase<T>.CreateValue(const AValue: Integer): T; var Foo: TFoo absolute Result; ... begin case GetTypeKind(T) of tkInteger: ... tkClass: begin System.Assert(TypeInfo(T) = TypeInfo(TFoo)); Foo := TFoo.Create; Foo.Value := AValue; end; ... end; end;
In this example, we only support classes of type TFoo
, so we check for that with an assertion.
Also, we are creating a new object here, so we should release it at some point. We could also write a generic ReleaseValue
method to take care of this:
procedure TTestCollectionBase<T>.ReleaseValue(const AValue: T); var Obj: TObject absolute AValue; ... begin case GetTypeKind(T) of tkClass: Obj.DisposeOf; ... end; end;
It uses the same absolute
variable “trick”.
For examples about using other custom types, such as (dynamic) arrays and records, take a look at the unit Tests.Grijjy.Collections.Base
in the Grijjy Foundation repository.
Different approaches
You may be using a different approach to test generic types. If so, leave a comment. We would be interested to hear what you came up with.
Erik, most of your blogs are about new things, it’s very valuable! I found Grijjy.MemoryPool.pas in your foundation repository, so when you planning your posts, would you consider give your memory pool implementation a priority 🙂
LikeLike
Thanks. I’m glad you like the blog. We didn’t plan a post on the memory pool yet, but since you’re interested, we will now!
LikeLike
Great! And I wish the memory pool works with objects as opposed to fixed-size types.
— Edwin Yip
LikeLike