To address some odd concerns about differences between DUnit and Smoketest, I thought it would be useful to demonstrate how it is entirely within the gift of a Smoketest user to create their own “comfort” layer, to make using Smoketest more similar to the DUnit framework if they wish (though why in that case they wouldn’t simply use DUnit, I can’t quite fathom. But still).
Interfaces vs and Inheritance
The great thing about Delphi is that when a class implements an interface any derived classes inherit that implementation. Even better, if you implement the interface methods virtually, then those derived classes can override the implementation without having to redeclare the interface.
So, if you really don’t like the idea of declaring and implementing interfaces to provide Setup and Cleanup methods on TTestCase derived classes you have a very simple remedy:
Implement your own class from which to derive your test cases and have this provide default, no-op, virtual implementations of these framework hooks, just as the DUnit test case base class does for the virtual methods it relies on.
If it really bothers you that much, you can even take this opportunity to replace the framework Cleanup method with a virtual TearDown method in it’s stead.
Heck, if you really want to write CheckEquals() for your test and then describe it separately, you can even provide that in your DUnit comfortable compatibility class. You can even have the CheckEquals() method always “fail early”, just like DUnit.
You might end up with something like this:
type TDUnitTestCase = class(TTestCase, ISetupTestCase, ICleanupTestCase) protected procedure Setup; virtual; procedure Cleanup; procedure TearDown; virtual; procedure CheckEquals(aResult: Boolean; aMsg: String = ''); end; procedure TDUnitTestCase.Setup; begin // NO-OP - just like mother used to make end; procedure TDUnitTestCase.Cleanup; begin TearDown; end; procedure TDUnitTestCase.TearDown; begin // NO-OP - the comfort of the familiar end; procedure TDUnitTestCase.CheckEquals(aResult: Boolean; aMsg: String); begin Test(aMsg).Expect(aResult).IsTRUE.IsShowStopper; end;
You could even implement the same pattern for the project and method specific setup and cleanup hooks. DUnit doesn’t have these so doesn’t provide a “specification” to comply with in this area, but you would presumably choose to follow the same virtual method pattern that DUnit adopts for the test case setup/teardown.
Why Not Just Copy DUnit ?
Some people don’t seem to understand why I didn’t simply mindlessly copy DUnit, thus making such adaptations unnecessary.
The thing is, I didn’t implement Smoketest with compatibility with DUnit in mind. Quite the opposite. I didn’t like the way DUnit worked! Little wonder then that Smoketest works differently!
I like to think however that the approach I took demonstrates it’s benefits no more obviously than when considering the ease with which it is possible to build such alternative API’s on top of the framework without having to resort to modifying the framework itself.
I imagine – though I have not tried – that you could even contrive to make such a compatibility layer so seamless and complete that an existing DUnit project would compile directly against Smoketest with only the slightest of changes, primarily around not having to explicitly state which “runner” you are using, since in Smoketest this is governed by the project compiler settings/directive directly (console vs GUI app) without the need for parallel changes to the project source code.
In the case of DUnit, it was impossible to achieve the changes in the test writing experience that I desired; you cannot reduce the visibility of inherited members, so with any pattern that relies on visibility of certain inherited class members you are stuck with the choices made in the implementation of the base class.
Smoketest doesn’t paint you into this particular corner.
For people – such as myself – who don’t like DUnit, Smoketest exists precisely to offer a choice. A choice albeit initially only I was making (in the sense of deciding how my preferred alternative would work differently).
If someone doesn’t like the choices I made it seems to me they themselves have a choice: They can – with a modicum of thought and a minimum of effort – add a layer of comfort that they will find acceptable. Or they can choose simply not to use Smoketest.
I would not presume to tell them what choice they should make. 😉
It could be that they have a lot of tests that run under DUnit but have found it too limiting. So they might be looking at something else (like Smoketest) but don’t have the time to convert all the current tests over to Smoketest, so an adapter between Smoketest and DUnit is handy.
A fair point. In which case, this example shows a way that could achieve that. 🙂
I have been only briefly following your posts on Smoketest, so I am still not in the position to fully evaluate your testing framework, but first impressions are rather good.
I have also found DUnit quite limiting and I am looking for replacements, but there is just too many existing tests to jump easily.
It looks like being more flexible could be Smoketest’s significant advantage over other frameworks, as well the fact it supports Delphi 7.
Keep up the good work!
Thanks. Given a seeming increase in interest in migration from DUnit to Smoketest I think I shall spend some time putting more flesh on the bones of this “DUnit Compatability” class to ease the transition for those interested. Or if somebody wishes to contribute such a class…. 🙂
Just don’t look at me 🙂
I would not mind contributing, but I am pretty sure that I would not have time to do so at least in next few months.
However, if and when I make my move from DUnit to Smoketest I will send you any improvements I might make.
One thing I’ve always found odd about DUnit (and all the other Delphi based unit test frameworks I’ve encountered) is that test projects are stand alone executables with the test runner baked into them rather than a library of tests that get loaded by a separate test runner.
In every .NET or Java test framework I’ve worked with the test runner is a precompiled executable that was either shipped with the test framework or provided by a third party and could run any suite of tests compiled against the test framework. This configuration opened the door for any number of different types of test runners: Command line, GUI, IDE extension, CI Server plugin or even a remote test runner. The .NET Gallio project could even run tests from multiple different test frameworks in the same session.
DUnit does support such a separation of concerns but it’s not the default, it’s poorly documented and it takes a while to setup. It assumes that the test runner and test libraries were compiled using the same ITest interface and that the libraries expose a function called Test that returns an instance of ITest. DUnit2, being a fork of DUnit also supports this configuration.
That I think is a function of the fact that Delphi doesn’t have a particularly robust model for dynamic object model extensions in the same way that .NET and Java do. Some dependency on/sensitivity to the same versions of Delphi being involved can be tricky to avoid if the aim is to make development of the extensions (i.e. the test libraries) “feel” like extending the framework.
But, funnily enough, that architecture is something that I have been considering recently. 🙂