[Estimated Reading Time: 5 minutes]

In my previous post on Smoketest I showed how you can extend the inspections framework to work with complex types in your code. As promised, I shall now show how you can do much the same thing to extend the framework with entirely new tests.

ā€œI must be taken as I have been made. The success is not mine, the failure is not mine, but the two together make me.ā€
ā€• Charles Dickens, Great Expectations

Well, you won’t have to take Smoketest entirely as it has been made. You can extend it. šŸ™‚

For a custom inspector an interface and a class are required. For a custom test (or tests) we need two interfaces and a class.

The first interface provides the methods which capture a value for which we will subsequently provide the test(s) in the second interface which must be returned by the capture method. Following the convention in the Smoketest framework, capture methods are called Expect().

Both interfaces must have an IID.

By convention the capture interface is named for the type being captured with a “Test” suffix. The interface providing the test(s) is named with an “Expectation” suffix:

    ComplexNumberTest = interface;
    ComplexNumberExpectation = interface;

    ComplexNumberTest = interface
    ['{A8FFAEFB-6F72-46F4-8E6C-68B770CC524B}']
      function Expect(aValue: TComplexNumber): ComplexNumberExpectation;
    end;

    ComplexNumberExpectation = interface
    ['{3B45B9FA-8647-4630-88A6-A30A2E0E026A}']
      function Equals(aValue: TComplexNumber): IEvaluation;
    end;

For the purposes of this example I shall implement only one test – for equality.

Test methods are expected (but not required) to yield an Evaluation interface as their result to allow a test author to append conditions such as IsRequired, IsCritical etc (this functionality is gained automatically, as long as you return this interface).

Although we need two interfaces, only one class is necessary to implement both of these. As with the inspector class, this must extend a specific base class which for tests and expectations is the TExpectation class. This class will hold the captured value being tested so we need a member variable for that:

    TComplexNumberTest = class(TExpectation, ComplexNumberTest,
                                             ComplexNumberExpectation)
    private
      fValue: TComplexNumber;
      property Value: TComplexNumber read fValue;

    public // ComplexNumberTest
      function Expect(aValue: TComplexNumber): ComplexNumberExpectation;

    public // ComplexNumberExpectation
      function Equals(aExpected: TComplexNumber): Evaluation; reintroduce;
    end;

NOTE: The reintroduce directive on the Equals() method is to silence the warning that results on more recent versions of Delphi where TObject introduces an Equals() method of it’s own.

The implementation of the Expect() method is straightforward. This method needs only to capture the value being tested (passed to the Expect() method) and return itself, since the tests are also implemented on the same object:

  function TComplexNumberTest.Expect(aValue: TComplexNumber): ComplexNumberExpectation;
  begin
    result := self;
    fValue := aValue;
    Actual := ComplexNumberToString(aValue);
  end;

In this instance as well as capturing the value to be subjected to any tests we also set a property of the TExpectation class called Actual. This is a string representation of the captured value which can be substituted in test output later.

The test method – Equals() – is similarly straightforward:

  function TComplexNumberTest.Equals(aExpected: TComplexNumber): IEvaluation;
  begin
    result := self;

    Description := '= {expected}';
    Expected    := ComplexNumberToString(aExpected);

    OK := (Value.Real = aExpected.Real)
      and (Value.Imaginary = aExpected.Imaginary);
  end;

Again, the TExpectation class already implements the necessary Evaluation interface allowing a test author to specify the criticality of any failure of the test if they require, so if we choose to yield the Evaluation interface all we have to do is return self again.

Other than that, it is helpful to set some further properties that will be used in the output of our test results.

For most tests it makes sense to set a Description string which describes the intent of the test. In this case we use a simple string '= {expected}'.

The token {expected} will be substituted by Smoketest (later) with the value of the second property we shall set: Expected. Similar to the Actual property, this is a simple string representation of the expected value, as passed to the Equals method.

The Description property is a string that is appended to any label specified by the test expression. Something to bear in mind is that if the test author has not specified a label then the value of Actual will be used.

How you format the Actual, Expected and Description strings for any given test is largely up to you. These are used only for the presentation of the results and are not involved in the test itself (there are some aspects of the way that results are presented which should be borne in mind but that is beyond the scope of this simple example).

That most critical aspect of a custom test is handled by setting the OK property.

This is a simple boolean that determines whether the test has passed or failed.

    OK := (Value.Real = aExpected.Real)
      and (Value.Imaginary = aExpected.Imaginary);

For a complex number equality test, the test passes if the real and imaginary parts of the two numbers are both equal.

The final step is to register the extension class and the test interface. The expectation interface is not involved in the registration process as the Smoketest framework has no need to even be aware of it’s existence, only the test interface is needed:

  Smoketest.RegisterExtension(ComplexNumberTest, TComplexNumberTest);

Just as with the inspector extension, the implementing class can be kept strictly as an implementation detail in an extension unit, if you wish.

With all that in place, test authors can now write tests for the TComplexNumber type almost as if they were part of the testing framework:

  var
    a, b: TComplexNumber;
  begin
    a.Real      := 1;
    a.Imaginary := 4;

    b := a;
    (Test as ComplexNumberTest).Expect(a).Equals(b);
  end;

Which results in the following output:

Equality!
Equality!

For completeness here is an example where the test is rigged to fail:

  var
    a, b: TComplexNumber;
  begin
    a.Real      := 1;
    a.Imaginary := 4;

    a.Real      := 1;
    a.Imaginary := 3;
    (Test as ComplexNumberTest).Expect(a).Equals(b);
  end;
All complex numbers are not equal
All complex numbers are not equal

An extension can implement as many methods on an expectation interface as it sees fit. In the case of Complex Numbers is might make sense – if this were a real extension – to provide tests for GreaterThan, LessThan, NotEqual etc.

All of these would be implemented by the same extension class and would simply need to set a Description more appropriate to the expectation being tested in each case and set the OK property to reflect the outcome of the particular test.

Pretty soon you will be able to play with the possibilities yourself as I hope to have the Smoketest framework with enough documentation to get people started ready to publish before the end of next week (though complete documentation may take longer).