Unit Testing Generic Types
January 10, 2017
January 10, 2017
[SHOWTOGROUPS=4,20]
So you created a generic class. Maybe a specialized generic collection, like a Для просмотра ссылки Войдиили Зарегистрируйся.
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:
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:
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:
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:
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:
[/SHOWTOGROUPS]
So you created a generic class. Maybe a specialized generic collection, like a Для просмотра ссылки Войди
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:
1 2 3 4 | type TTestCollectionBase<T> = class abstract ... end; |
Then you derive your test class from this base class, still as a generic class:If you are using DUnit instead of DUnitX, everything in this post still applies. Just derive the base class from TTestCase.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | 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:
1 2 3 4 5 6 7 8 9 10 11 12 | 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:
1 2 3 4 5 6 7 8 9 | 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:
1 2 3 4 5 | 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:
1 2 3 4 5 | type TTestCollectionBase<T> = class abstract protected function CreateValue(const AValue: Integer): T; end; |
[/SHOWTOGROUPS]