Foundation · Tips & Tricks · Uncategorized

Unit Testing Generic Types

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 return AValue.
  • If <T> is a string type, then return IntToStr(AValue).
  • If <T> s a TFoo class, then create an instance of the TFoo class, set its Value property to AValue 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;

Variables in a linear layout

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;

Variables in a stacked layout

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.

4 thoughts on “Unit Testing Generic Types

  1. 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 🙂

    Like

    1. 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!

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s