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.