Как получить доступ к полям TTestCase в классе TTestSetup
Я создаю модульные тесты с DUnit. У меня есть класс, который занимает довольно много времени для инициализации.
Я извлекаю класс TMyTestSetup из TTestSetup и переопределяю его метод установки. Этот метод SetUp вызывается только один раз для всех тестов в моем TTestCase. Я поместил процесс инициализации в процедуру TMyTestSetup.SetUp для повышения производительности.
Моя проблема в том, как я могу получить доступ к объекту, который я хочу инициализировать, который является полем моего TMyTest в классе TestSetup? Единственный способ сделать это, объявив это глобально?
непроверенный короткий пример:
TMyTestSetup = class(TTestSetup)
protected
procedure SetUp; override;
end;
TMyTest = class(TTestcase)
public
fTakes4Ever2Init : TInits4Ever2Init;
published
procedure Test1;
end;
implementation
procedure TMyTestSetup.Setup;
begin
// How can I access fTakes4Ever2Init from here?
fTakes4Ever2Init.create // This is the call that takes long
end;
procedure TMyTest.Test1;
begin
fTakes4Ever2Init.DoSomething;
end;
initialization
RegisterTest(TMyTestSetup.Create(TMyTest.Suite));
6 ответов
Хитрость заключается в том, чтобы использовать переменную открытого класса в классе TMyTestSetup.
Вот так (проверено и работает, завершено) пример:
unit TestTestUnit;
interface
uses
TestFramework, TestExtensions;
type
TInits4Ever2Init = class
private
FValue: integer;
public
constructor Create;
procedure DoSomething1;
procedure DoSomething2;
procedure DoSomething3;
end;
type
TMyTestSetup = class(TTestSetup)
public class var
fTakes4Ever2Init: TInits4Ever2Init;
protected
procedure SetUp; override;
end;
TMyTest = class(TTestCase)
published
procedure Test1;
procedure Test2;
procedure Test3;
end;
implementation
uses
SysUtils, Windows;
{ TMyTestSetup }
procedure TMyTestSetup.Setup;
begin
fTakes4Ever2Init := TInits4Ever2Init.create; // This is the call that takes long
end;
{ TMyTest }
procedure TMyTest.Test1;
begin
TMyTestSetup.fTakes4Ever2Init.DoSomething1;
end;
procedure TMyTest.Test2;
begin
TMyTestSetup.fTakes4Ever2Init.DoSomething2;
end;
procedure TMyTest.Test3;
begin
TMyTestSetup.fTakes4Ever2Init.DoSomething3;
end;
{ TInits4Ever2Init }
constructor TInits4Ever2Init.Create;
begin
inherited Create;
// FValue and Format('%p, %d', [Pointer(Self), FValue])) are to confirm
// that we are talking to the same object for all the tests,
// but that the object is different each time we run the test suite.
Randomize;
FValue := Random(10000);
OutputDebugString(pAnsiChar('-- TInits4Ever2Init.Create: '
+ Format('%p, %d', [Pointer(Self), FValue])));
end;
procedure TInits4Ever2Init.DoSomething1;
begin
OutputDebugString(pAnsiChar('-- TInits4Ever2Init.DoSomething1: '
+ Format('%p, %d', [Pointer(Self), FValue])));
end;
procedure TInits4Ever2Init.DoSomething2;
begin
OutputDebugString(pAnsiChar('-- TInits4Ever2Init.DoSomething2: '
+ Format('%p, %d', [Pointer(Self), FValue])));
end;
procedure TInits4Ever2Init.DoSomething3;
begin
OutputDebugString(pAnsiChar('-- TInits4Ever2Init.DoSomething3: '
+ Format('%p, %d', [Pointer(Self), FValue])));
end;
initialization
RegisterTest(TMyTestSetup.Create(TMyTest.Suite));
end.
Как показывают комментарии в примере, я использовал рандомизированную частную переменную и некоторые выходные данные трассировки отладки, чтобы подтвердить, что каждый вызов теста с набором тестов относится к одной и той же копии целевого объекта, но мы получаем другую копию целевого объекта при каждом запуске набора тестов.
Вы можете получить новый класс Test Suite из класса TTestSuite и переопределить его методы SetUp и TearDown, а затем добавить тестовые случаи в этот конкретный набор тестов и зарегистрировать набор.
Таким образом, методы Setup и TearDown вашего класса набора тестов будут вызываться один раз, а методы SetUp и TearDown каждого теста будут вызываться для каждого метода теста, определенного в этом тесте.
Порядок исполнения будет таким:
TestSuite.SetUp;
-- TestCase1.Setup;
---- TestCase1.Test1;
-- TestCase1.TearDown;
-- TestCase1.Setup;
---- TestCase1.Test2;
-- TestCase1.TearDown;
-- TestCase2.Setup;
---- TestCase2.Test1;
-- TestCase2.TearDown;
-- TestCase2.Setup;
---- TestCase2.Test2;
-- TestCase2.TearDown;
-- TestCaseN.Setup;
---- TestCaseN.Test1;
-- TestCaseN.TearDown;
-- TestCaseN.Setup;
---- TestCaseN.Test2;
-- TestCaseN.TearDown;
TestSuite.TearDown;
Наличие только одного опубликованного метода, который, в свою очередь, вызывает все остальные методы тестирования, является ленивым, но более быстрым способом вызова процедур Setup и TearDown только один раз.
Вы не можете инициализировать поля TTestCase для всего набора тестов, и вот объяснение, почему:
unit Tests3;
interface
uses
TestFramework, TestExtensions, Windows, Forms, Dialogs, Controls, Classes,
SysUtils, Variants, Graphics, Messages;
type
TMyTestCase = class(TTestCase)
private
FValue: Integer;
published
procedure Test1;
procedure Test2;
end;
implementation
{ TMyTestCase }
procedure TMyTestCase.Test1;
begin
FValue:= 99;
ShowMessage(Format('%p, %d', [Pointer(Self), FValue]));
end;
procedure TMyTestCase.Test2;
begin
ShowMessage(Format('%p, %d', [Pointer(Self), FValue]));
end;
initialization
RegisterTest(TMyTestCase.Suite);
end.
Если вы запустите вышеупомянутый модульный тест, вы увидите, что адреса "Self", показанные в методах Test1 и Test2, отличаются. Это означает, что экземпляры объекта TMyTestCase отличаются для вызовов Test1 и Test2.
Следовательно, любые поля, которые вы можете объявить в классе TMyTestCase, являются нестабильными между вызовами тестового метода.
Чтобы выполнить "глобальную" инициализацию, вы должны объявить свой объект глобально, а не как поле TMyTestCase.
С помощью TTestSetup
Вы могли бы сделать что-то вроде этого:
type
TMyTestSetup = class(TTestSetup)
private
FValue: Integer;
protected
procedure SetUp; override;
procedure TearDown; override;
end;
TMyTestCase = class(TTestCase)
published
procedure TestSomething;
end;
var
TestSetup: TMyTestSetup;
procedure TMyTestSetup.SetUp;
begin
inherited;
TestSetup := Self;
FValue := 42;
end;
procedure TMyTestSetup.TearDown;
begin
TestSetup := nil;
inherited;
end;
procedure TMyTestCase.TestSomething;
begin
CheckEquals(TestSetup.FValue, 42);
end;
initialization
TestFramework.RegisterTest(TMyTestSetup.Create(
TTestSuite.Create('My test suite', [TMyTestCase.Suite])
));
Это отвратительно, но это делает работу!
В зависимости от версии Delphi вы можете просто TMyTest.fTakes4Ever2Init
поле а public class var
чтобы инициализировать его из настройки теста. (Это был бы более стиль ООП по сравнению с глобальной единицей.)
Лучшее решение (... ИМХО)
Это довольно старый вопрос, но я могу представить, как люди все еще сталкиваются с этим. Я сделал.
В моем первоначальном решении этой проблемы также использовались переменные класса или глобальные переменные. Но на самом деле это решение плохое, так как затрудняет повторное использование.
TTestSetup
производные классы. Поэтому я немного отлаживал, чтобы понять, как DUnit работает внутри. (Я широко использую DUnit в своем флагманском приложении и библиотеках)
Как оказалось, вы действительно можете получить доступ к субтестам: изнутри
TTestSetup.RunTest
. В этом методе вы получаете дескриптор завернутого / декорированного подтеста, который на самом деле оказался
TTestSuite
, созданный из моих
TTestCase.Suite
. Итак, я перебираю
ITestsuite
подтесты (которые на самом деле являются вызовами каждого опубликованного метода в вашем
TtestCase
) и проверьте, поддерживают ли они мои
ITestDecoratable
интерфейс, в таком случае я называю
SetupDecoration
.
Затем выполняется фактический тест, вызывая
inherited Runtest
.
И, наконец, мы снова проходим тот же цикл, на этот раз вызывая
TearDownDecoration
.
Это не исправило вложенные
TTestsetup
случае, поэтому я добавил проверку, если
TTestDecorator.Test
поддерживает
ITestDecoratable
напрямую и выполняйте соответственно. В этом отношении я также реализовал
ITestDecoratable
в моем
TDecoratedTestSetup
так что вложение также поддерживается.
И придумал это решение. Я даже создал для него модульный тест, и все работает как задумано.
Я могу представить, что лучше реализовать эти методы в
TTestCase
и
TTestDecorator
напрямую, а пока я вынес его в отдельный блок. Я добавлю билет на соответствующий сайт sourceforge.
Вот мое решение:
unit uDecoratorTestBase;
interface
uses TestFramework,TestExtensions;
type
/// <summary>
/// when a test implements the interface below, and the TDecoratedTestSetup
/// is used, these methods get called dureing testing.
/// </summary>
ITestDecoratable=interface (ITest)
['{468A66E9-937B-4C45-9321-A1796F93470C}']
/// <summary>
/// gets called before the Setup call
/// </summary>
procedure SetupDecoration(const aDecorator:ITestDecorator);
/// <summary>
/// gets called after the teardown call
/// </summary>
procedure TeardownDecoration(const aDecorator:ITestDecorator);
end;
/// <summary>
/// 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
/// </summary>
TDecoratedTestSetup=class(TTestDecorator,ITestDecoratable)
private
protected
procedure RunTest(ATestResult: TTestResult); override;
procedure SetupDecoration(const aDecorator:ITestDecorator); virtual;
procedure TeardownDecoration(const aDecorator:ITestDecorator); virtual;
end;
/// <summary>
/// Same as TTestcase, but adds the ITestDecoratable interface. Override
/// the routines below to get values from the decorator class through
/// the provided ITestDecorator interface.
/// </summary>
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 UnitTestDecorator;
interface
uses
TestFrameWork,uDecoratorTestBase;
type
/// <summary>
/// Perofms the actuel self-test by running decorated testcases
/// </summary>
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.