In the soon to be released Smoketest framework it is sometimes useful to create new test types to supplement the tests built-in to the framework. In this and the next post I will walk through the process of implementing and registering a custom test with the Smoketest framework.
In a real-world scenario this may be any complex business object type which we have a set of tests we regularly apply in addition to the tests we may perform on fundamental types using the tests built-in to Smoketest itself.
For this exercise I shall use the example of a Complex Number test extension that was implemented specifically to exercise this aspect of Smoketest as part of it’s own self-test test suite.
First let us define the ComplexNumber type that we wish to test:
type TComplexNumber = record Real: Double; Imaginary: Double; end;
As well as test extensions we can also implement custom inspectors.
These are a little more straightforward than tests but the process involved is fundamentally similar so by way of a gentle introduction let’s do an inspector first.
The first step is to define an interface for our inspector.
There are no requirements for this interface other than it should have an IID and define the methods we will provide for inspecting our custom type. The convention in Smoketest is to have a Value() method for inspecting the type, but we can define whatever methods we feel appropriate.
In this case I define three methods: Value() plus two further methods for inspecting the Real() and Imaginary() components of a TComplexNumber individually:
ComplexNumberInspector = interface ['{614CD588-718D-4950-8A5C-7F8252613487}'] procedure Real(aValue: TComplexNumber); procedure Imaginary(aValue: TComplexNumber); procedure Value(aValue: TComplexNumber); end;
Now we need to declare a class that implements this interface. This class must extend the TInspector class from Smoketest.
TComplexNumberInspector = class(TInspector, ComplexNumberInspector) public procedure Real(aValue: TComplexNumber); procedure Imaginary(aValue: TComplexNumber); procedure Value(aValue: TComplexNumber); end;
We register this extension class with Smoketest in the unit initialization section. This is the only occasion that the class needs to be referenced so by registering it from within the unit itself the entire class implementation can be kept in, well, the implementation section. Only the interface needs to be in, well, the interface section.
Registering a custom inspector with Smoketest is achieved by calling the RegisterExtension() method identifying the interface and the implementing class:
initialization Smoketest.RegisterExtension(ComplexNumberInspector, TComplexNumberInspector); end.
Now to implement the inspector methods themselves.
An inspector method simply Emit()s some string which is captured by Smoketest for presentation in the test results, so each of the ComplexNumberInspector methods has a very simple job of emitting a suitably presented string.
In addition to these methods I also implement a helper routine to format an entire ComplexNumber as a string which will come in handy later when I also implement the test extension.
NOTE: A simple function is used for this helper rather than a record helper since this is part of self-test code which must be compatible with Delphi versions as far back as Delphi 7.
function ComplexNumberToString(const aValue: TComplexNumber): String; begin result := Format('[%f + %fi]', [aValue.Real, aValue.Imaginary]); end; procedure TComplexNumberInspector.Real(aValue: TComplexNumber); begin Emit(FloatToStr(aValue.Real)); end; procedure TComplexNumberInspector.Imaginary(aValue: TComplexNumber); begin Emit(FloatToStr(aValue.Imaginary)); end; procedure TComplexNumberInspector.Value(aValue: TComplexNumber); begin Emit(ComplexNumberToString(aValue)); end;
That’s it.
With this inspector interface defined, the class implemented and both registered as an extension with Smoketest, I can now inspect ComplexNumber values in any of my test cases by obtaining the required extension from an inspector (obtained via a call to the Inspect() method, you may recall):
var a: TComplexNumber; begin a.Real := 1; a.Imaginary := 4; (Inspect('Complex Number') as ComplexNumberInspector).Value(a); (Inspect('Complex Number')['real'] as ComplexNumberInspector).Real(a); (Inspect('Complex Number')['imaginary'] as ComplexNumberInspector).Imaginary(a); end;
This can get a bit “wordy” and cumbersome though could still represent a benefit for more complex custom types than this simple ComplexNumber example. Never-the-less I have some ideas for allowing this to be simplified further (the fundamentals of which are already working though the sub-indexed label feature is presenting a challenge which I am currently working on).
Anyhow, for now (and, it is hoped, with the simplified version in the future) the resulting output from these inspections looks like this:
In the next post I’ll show how the same basic approach (with a small twist) allows me to add more useful extensions that create new expectations which can be used to test values of such custom types.