Comment accéder aux champs d'un TTestCase dans une classe TTestSetup
-
29-10-2019 - |
Question
Je crée des tests unitaires avec DUnit.J'ai une classe qui prend beaucoup de temps à s'initialiser.
Je dérive une classe TMyTestSetup de TTestSetup et écrase sa méthode Setup.Cette méthode SetUp n'est appelée qu'une seule fois pour tous les tests de mon TTestCase.J'ai mis le processus d'initialisation dans la routine TMyTestSetup.SetUp pour augmenter les performances.
Mon problème est de savoir comment accéder à l'objet que je souhaite initialiser, qui est un champ de mon TMyTest dans la classe TestSetup?Est-ce que le seul moyen de le faire est de le déclarer globalement?
exemple court non testé:
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));
La solution
L'astuce consiste à utiliser une variable de classe publique dans la classe TMyTestSetup.
Comme cet exemple (testé et fonctionnel, complet):
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.
Comme l'indiquent les commentaires de l'exemple, j'ai utilisé une variable privée aléatoire et une sortie de trace de débogage, pour confirmer que chaque appel de test avec la suite de tests concerne la même copie de l'objet cible, mais que nous obtenonsune copie différente de l'objet cible à chaque exécution de la suite de tests.
Autres conseils
Vous pouvez dériver une nouvelle classe de Test Suite à partir de la classe TTestSuite et remplacer ses méthodes SetUp et TearDown, puis vous pouvez ajouter vos cas de test à cette suite de tests particulière et enregistrer la suite.
De cette façon, les méthodes Setup et TearDown de votre classe de suite de tests seront appelées une fois, et les méthodes SetUp et TearDown de chaque cas de test seront appelées pour chaque méthode de test définie dans ce cas de test.
L'ordre d'exécution sera comme ceci:
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;
Le fait de n'avoir qu'une seule méthode publiée, qui à son tour appelle toutes vos autres méthodes de test, est le moyen paresseux mais plus rapide d’appeler une seule fois la procédure Setup et TearDown.
Vous ne pouvez pas initialiser les champs TTestCase pour toute une suite de tests, et voici une explication pourquoi:
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.
Si vous exécutez le test unitaire ci-dessus, vous verrez que les adresses 'Self' affichées dans les méthodes Test1 et Test2 sont différentes.Cela signifie que les instances d'objet TMyTestCase sont différentes pour les appels Test1 et Test2.
Par conséquent, tous les champs que vous pouvez déclarer dans la classe TMyTestCase sont volatils entre les appels de la méthode de test.
Pour effectuer une initialisation "globale", vous devez déclarer votre objet globalement, pas en tant que champ TMyTestCase.
En utilisant TTestSetup
, vous pouvez faire quelque chose comme ceci:
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])
));
Cela vous semble quelque peu révoltant, mais cela fait le travail!
En fonction de votre version de Delphi, vous pouvez simplement faire du champ TMyTest.fTakes4Ever2Init
un public class var
pour l'initialiser à partir de la configuration de test.(Ce serait plus de style POO par rapport à une variable globale d'unité.)