Every so often this comes up: I have an interface. It has methods. I should unit test everything. So I should test this interface. But how? It doesn’t have an implementation…?!
Right…
Well…
Hmm…
No. Of course you don’t test the interface. You can’t. It has no implementation.
But…
You do want to test each and every class that implements this interface.
To check that any class that implements the interface meets the expectations of the clients of that interface.
Okay…
So how do you go about that?
After all, you want to keep your tests DRY. So you certainly don’t want to write all the tests with regard to the interface all over again for each new implementer of the interface.
It depends…
Yeah I know. Sounds like a cop out. But in this case it really depends on the unit test framework you use and what it enables you to do.
What holds true regardless of which language and framework you use is that you create a single test class to hold all the tests that you want to run for all implementers of your interface. And you code all the methods in that test class using a reference to the interface (instead of a reference to a specific class).
Sounds a bit complicated, but it is fairly straightforward.
Say you have an interface ISomeInterface
with a single method SomeMethod
.
ISomeInterface = interface(IInterface) function SomeMethod(const aInput: string): Boolean; end;
And you want to ensure that any implementer of ISomeInterface
returns false
when it is passed an empty string. Somewhat like:
CheckEquals(FInterfaceReference.SomeMethod(''), False);
NUnit
Using C# and NUnit you would just write a single generic class, give it the test methods you need to ensure implementers meet the desired behavior and then decorate it with [TestFixture(typeof(SomeImplementation))]
attributes. One attribute for each implementing class. That’s all.
[TestFixture(typeof(MyImplementerOfSomeInterface))] [TestFixture(typeof(MyOtherImplementerOfSomeInterface))] public class TestsOfISomeInterface<T> where T : ISomeInterface, new() { ISomeInterface impl; [SetUp] public void CreateISomeInterfaceImplementation() { impl = new T(); } // Use impl in your tests }
DUnitX
In Delphi with DUnitX I expect you could do much the same thing as DUnitX is modelled on NUnit.
(I haven’t had the pleasure of using DUnitX yet. Please chime in with a comment if you know for sure.)
DUnit on generics
If you are using a Delphi version which supports generics, you can do much the same thing using DUnit. You start by declaring a generic test class for the tests on implementers of ISomeInterface
.
type TSomeInterfaceContractTests<T: TInterfacedObject, constructor> = class(TTestCase) strict private FInterfaceUnderTest: ISomeInterface; protected procedure SetUp; override; published procedure SomeMethod_Input_EmptyString_ShouldReturn_False; end; procedure TSomeInterfaceContractTests<T>.SetUp; begin inherited; Supports(T.Create, ISomeInterface, FInterfaceUnderTest); Assert(Assigned(FInterfaceUnderTest), 'Generic type does not support ISomeInterface'); end; procedure TSomeInterfaceContractTests<T>.SomeMethod_Input_EmptyString_ShouldReturn_False; begin CheckEquals(FInterfaceUnderTest.SomeMethod(''), False); end;
I am sure you could do some clever magic using extended RTTI and put attributes on this class to register specific test classes for your classes that implement ISomeInterface
. But in that case you are probably better off switching from DUnit to DUnitX. Besides which it is hardly any more trouble to register your implementing classes to be tested without using attributes. All you need do is:
RegisterTest(TSomeInterfaceContractTests<TMyImplementerOfSomeInterface>.Suite); RegisterTest(TSomeInterfaceContractTests<TMyOtherImplementerOfSomeInterface>.Suite);
DUnit straight up
If you are using a Delphi version which does not support generics, then you need to do a bit more work yourself, but the basics are the same.
You create a “contract” test case class that contains the test methods to ensure an implementer of ISomeInterface
meets the expected behavior.
type TSomeInterfaceContractTests = class(TTestCase) strict private FInterfaceUnderTest: ISomeInterface; published procedure SomeMethod_Input_EmptyString_ShouldReturn_False; end; TSomeInterfaceContractTests.SomeMethod_Input_EmptyString_ShouldReturn_False; begin CheckEquals(FInterfaceUnderTest.SomeMethod(''), False); end;
To instantiate the proper class for your test methods you have several options. The one I like best uses the beauty of Delphi’s meta classes. For this you add a virtual abstract method to your contract test case.
type TSomeInterfaceContractTests = class(TTestCase) strict protected function GetImplementingClass: TInterfacedClass; virtual; abstract;
And use this in your SetUp
override or in some other method you use to instantiate the class under test:
TSomeInterfaceContractTests.SetUp; begin inherited; Supports(GetImplementingClass.Create, ISomeInterface, FInterfaceUnderTest); Assert(Assigned(FInterfaceUnderTest), 'Implementing class does not support ISomeInterface'); end;
Derived test classes must then of course override the GetImplementingClass
method and return the class of the implementing class they are intended to exercise.
type TMyImplemterOfSomeInterfaceTest = class(TSomeInterfaceContractTests) strict protected function GetImplementingClass: TInterfacedClass; override; TMyImplemterOfSomeInterfaceTest.GetImplementingClass: TInterfacedClass; begin Result := TMyImplementerOfSomeInterface; end;
Now all that is left to do is to register your derived test case(s) so that the test runner will execute all the test methods declared in the contract tests.
RegisterTest(TMyImplemterOfSomeInterfaceTest.Suite);
And when you have another class implementing the ISomeInterface
interface. All you need to do to have it tested for its implementation of that interface is:
- derive a test case class for that specific class from
TSomeInterfaceContractTest
- override the
GetImplementingClass
method - register the test case with DUnit.
Even without generics, you have full interface mocking and stubbing with mORMot.
See http://synopse.info/files/html/Synopse%20mORMot%20Framework%20SAD%201.18.html#TITL_62
It has unique features and full integration with our test classes.
Sounds nice. Tell me though, why would I want to get a full blown ORM (Object Relational Mapper) if I only want stubbing and mocking? I don’t much like getting a house when I just need a bathroom? Now, if the stubbing and mocking features were available separately…
No, of course the ORM has nothing to do with those stubs and mocks.
Mocking is implemented in the same main unit, mORMot.pas, but both features are not linked to each other. Only some low-level optimized “fake class instance” creation, and low-level JSON process (used for the execution tracing, which is quite unique among other Delphi stubbing frameworks AFAIK), are shared.
Thanks to Delphi compiler smart linking, you can use stubbing/mocking features without having any link to the ORM code of the framework in your executable.
This feature works from Delphi 6 up to latest XE7, by the way – so you can also use it with legacy apps.