I am creating unit tests with DUnit. I have a class that takes quite a long time to initialize.
I derive a class TMyTestSetup from TTestSetup and override its Setup met
The better solution (... IMHO)
It's a pretty old question, but I can imagine people still bumping into this. I did.
My initial solution to this problem also used class vars or globals. But indeed this solution is bad as it makes it very hard to re-use TTestSetup
derived classes. Hence I debugged a bit to find how DUnit works internally. (I use DUnit extensively on my flagship app and libs)
As it turns out you actually can get access to the subtests: from within TTestSetup.RunTest
. In this method you get a handle to the wrapped/decorated Subtest, which actually turned out to be a TTestSuite
, created from my TTestCase.Suite
. So I loop through the ITestsuite
subtests (which are actually method calls for each published method in your TtestCase
), and check if they support my ITestDecoratable
interface, if so I call the SetupDecoration
.
Next, the actual test is performed by calling the inherited Runtest
.
And finally we go through the same loop again, this time calling TearDownDecoration
.
This did not fix the nested TTestsetup
case, so I added a check if TTestDecorator.Test
supports ITestDecoratable
directly, and execute accordingly. For that matter, I alsom implemented the ITestDecoratable
in my TDecoratedTestSetup
so nesting is also supported.
And came up with this solution. I even created a unit test for it, and everything works as intended.
I can imagine one would rather implement these methods in TTestCase
and TTestDecorator
directly, but for now I have put it in a separate unit. I'll add a ticket to the corresponding sourceforge site.
Here's my solution:
unit uDecoratorTestBase;
interface
uses TestFramework,TestExtensions;
type
///
/// when a test implements the interface below, and the TDecoratedTestSetup
/// is used, these methods get called dureing testing.
///
ITestDecoratable=interface (ITest)
['{468A66E9-937B-4C45-9321-A1796F93470C}']
///
/// gets called before the Setup call
///
procedure SetupDecoration(const aDecorator:ITestDecorator);
///
/// gets called after the teardown call
///
procedure TeardownDecoration(const aDecorator:ITestDecorator);
end;
///
/// an alternatine to TTestSetup this implementation tries to decorate
/// any subtests when it is executed through the ITestDecoratable interface
/// bonus feature is that iself also supports the ItestDecoratable interface
/// allowing for multiple layes of decoration
///
TDecoratedTestSetup=class(TTestDecorator,ITestDecoratable)
private
protected
procedure RunTest(ATestResult: TTestResult); override;
procedure SetupDecoration(const aDecorator:ITestDecorator); virtual;
procedure TeardownDecoration(const aDecorator:ITestDecorator); virtual;
end;
///
/// Same as TTestcase, but adds the ITestDecoratable interface. Override
/// the routines below to get values from the decorator class through
/// the provided ITestDecorator interface.
///
TDecoratedTestCase=class(TTestCase,ITestDecoratable)
protected
procedure SetupDecoration(const aDecorator:ITestDecorator); virtual;
procedure TeardownDecoration(const aDecorator:ITestDecorator); virtual;
end;
implementation
uses
sysutils;
{ TDecoratedTestSetup }
procedure TDecoratedTestSetup.RunTest(ATestResult: TTestResult);
var lDecoratable:ITestDecoratable;
var lSuite:ITestSuite;
begin
if Supports(Test,ITestDecoratable,lDecoratable) then
try
lDecoratable.SetupDecoration(self);
inherited;
finally
lDecoratable.TeardownDecoration(self);
end
else if Supports(Test,ITestSuite,lSuite) then
try
for var I := 0 to lSuite.Tests.Count-1 do
if Supports(lSuite.Tests[i],ITestDecoratable,lDecoratable) then
lDecoratable.SetupDecoration(self);
inherited;
finally
for var I := 0 to lSuite.Tests.Count-1 do
if Supports(lSuite.Tests[i],ITestDecoratable,lDecoratable) then
lDecoratable.TeardownDecoration(self);
end
else inherited;
end;
procedure TDecoratedTestSetup.SetupDecoration(const aDecorator: ITestDecorator);
begin
// override to initialize class fields using the decorator
end;
procedure TDecoratedTestSetup.TeardownDecoration(const aDecorator: ITestDecorator);
begin
// override to finalize class fields previously initialized through SetupDecoration
end;
{ TDecoratedTestCase }
procedure TDecoratedTestCase.SetupDecoration(const aDecorator: ITestDecorator);
begin
// override to initialize class fields using the decorator
end;
procedure TDecoratedTestCase.TeardownDecoration(
const aDecorator: ITestDecorator);
begin
// override to finalize class fields previously initialized through SetupDecoration
end;
end.
Unit Test
And here's the unit test I created for my solution. Running this should shed some light and hopefully make you understand what's going on.
unit UnitTestDecorator;
interface
uses
TestFrameWork,uDecoratorTestBase;
type
///
/// Perofms the actuel self-test by running decorated testcases
///
TTestDecoratorTest=class(TTestCase)
private
protected
procedure SetUp; override;
published
procedure TestDecorated;
end;
implementation
type
TMyDecoratedTestCase=class(TDecoratedTestCase)
private
class var FDecorateCalls:integer;
class var FUndecorateCalls:integer;
protected
procedure SetupDecoration(const aDecorator:ITestDecorator); override;
procedure TeardownDecoration(const aDecorator:ITestDecorator); override;
procedure Setup; override;
procedure TearDown; override;
published
procedure CheckSetupTearDown;
procedure FailTest;
end;
TMyInnerDecoratedTestSetup=class(TDecoratedTestSetup)
private
class var FDecorateCalls:integer;
class var FUndecorateCalls:integer;
protected
procedure SetupDecoration(const aDecorator:ITestDecorator); override;
procedure TeardownDecoration(const aDecorator:ITestDecorator); override;
procedure Setup; override;
procedure TearDown; override;
published
procedure CheckSetupTearDown;
end;
TMyOuterDecoratedTestSetup=class(TDecoratedTestSetup)
private
class var FDecorateCalls:integer;
class var FUndecorateCalls:integer;
protected
procedure SetupDecoration(const aDecorator:ITestDecorator); override;
procedure TeardownDecoration(const aDecorator:ITestDecorator); override;
published
procedure CheckSetupTearDown;
end;
{ TTestDecoratorTest }
procedure TTestDecoratorTest.Setup;
begin
inherited;
TMyDecoratedTestCase.FDecorateCalls:=0;
TMyDecoratedTestCase.FUndecorateCalls:=0;
TMyInnerDecoratedTestSetup.FDecorateCalls:=0;
TMyInnerDecoratedTestSetup.FUndecorateCalls:=0;
TMyOuterDecoratedTestSetup.FDecorateCalls:=0;
TMyOuterDecoratedTestSetup.FUndecorateCalls:=0;
end;
procedure TTestDecoratorTest.TestDecorated;
begin
var lTestCaseSuite:=TMyDecoratedTestCase.Suite;
var lInnerTestSetup:=TMyInnerDecoratedTestSetup.Create(lTestCaseSuite) as ITest;
var lOuterTestSetup:=TMyOuterDecoratedTestSetup.Create(lInnerTestSetup) as ITest;
var lTestResult:=TTestResult.Create;
try
lOuterTestSetup.RunTest(lTestResult);
CheckEquals(0,lTestResult.ErrorCOunt,'lTestResult.ErrorCOunt');
CheckEquals(1,lTestResult.FailureCOunt,'lTestResult.FailureCOunt');
finally
lTestResult.Free;
end;
CheckEquals(2,TMyDecoratedTestCase.FDecorateCalls,'TMyDecoratedTestCase.FDecorateCalls');
CheckEquals(TMyDecoratedTestCase.FDecorateCalls,TMyDecoratedTestCase.FUndecorateCalls,'TMyDecoratedTestCase.FUndecorateCalls');
CheckEquals(1,TMyInnerDecoratedTestSetup.FDecorateCalls,'TMyInnerDecoratedTestSetup.FDecorateCalls');
CheckEquals(TMyInnerDecoratedTestSetup.FDecorateCalls,TMyInnerDecoratedTestSetup.FUndecorateCalls,'TMyInnerDecoratedTestSetup.FUndecorateCalls');
CheckEquals(0,TMyOuterDecoratedTestSetup.FDecorateCalls,'TMyOuterDecoratedTestSetup.FDecorateCalls');
CheckEquals(TMyOuterDecoratedTestSetup.FDecorateCalls,TMyOuterDecoratedTestSetup.FUndecorateCalls,'TMyOuterDecoratedTestSetup.FUndecorateCalls');
end;
{ TMyDecoratedTestCase }
procedure TMyDecoratedTestCase.CheckSetupTearDown;
begin
CheckNotEquals(0,FDecorateCalls,'FDecorateCalls');
CheckEquals(0,FUnDecorateCalls,'FUnDecorateCalls');
end;
procedure TMyDecoratedTestCase.FailTest;
begin
Fail('Intentionally');
end;
procedure TMyDecoratedTestCase.Setup;
begin
inherited;
CheckNotEquals(0,FDecorateCalls,'FDecorateCalls'); // decorate must take place BEFORE setup
end;
procedure TMyDecoratedTestCase.SetupDecoration(
const aDecorator: ITestDecorator);
begin
inherited;
inc(FDecorateCalls);
end;
procedure TMyDecoratedTestCase.TearDown;
begin
inherited;
CheckEquals(0,FUnDecorateCalls,'FUnDecorateCalls'); // undecorate must take place AFTER Teardown
end;
procedure TMyDecoratedTestCase.TeardownDecoration(
const aDecorator: ITestDecorator);
begin
inherited;
inc(FUnDecorateCalls);
end;
{ TMyInnerDecoratedTestSetup }
procedure TMyInnerDecoratedTestSetup.CheckSetupTearDown;
begin
CheckNotEquals(0,FDecorateCalls,'FDecorateCalls');
CheckEquals(0,FUnDecorateCalls,'FUnDecorateCalls');
end;
procedure TMyInnerDecoratedTestSetup.Setup;
begin
inherited;
CheckNotEquals(0,FDecorateCalls,'FDecorateCalls'); // decorate must take place BEFORE setup
end;
procedure TMyInnerDecoratedTestSetup.SetupDecoration(
const aDecorator: ITestDecorator);
begin
inc(FDecorateCalls);
inherited;
end;
procedure TMyInnerDecoratedTestSetup.TearDown;
begin
inherited;
CheckEquals(0,FUnDecorateCalls,'FUnDecorateCalls'); // undecorate must take place AFTER Teardown
end;
procedure TMyInnerDecoratedTestSetup.TeardownDecoration(
const aDecorator: ITestDecorator);
begin
inherited;
inc(FUnDecorateCalls);
end;
{ TMyOuterDecoratedTestSetup }
procedure TMyOuterDecoratedTestSetup.CheckSetupTearDown;
begin
CheckEquals(0,FDecorateCalls);
CheckEquals(0,FUnDecorateCalls);
end;
procedure TMyOuterDecoratedTestSetup.SetupDecoration(
const aDecorator: ITestDecorator);
begin
inherited;
inc(FDecorateCalls);
end;
procedure TMyOuterDecoratedTestSetup.TeardownDecoration(
const aDecorator: ITestDecorator);
begin
inherited;
inc(FUnDecorateCalls);
end;
initialization
RegisterTests('Decorator Test setup extensions for DUnit',
[
TTestDecoratorTest.Suite
]);
end.