Illustration en Delphi du pattern Test Data Builder
Date de publication : 21 avril 2010
Par
Bruno Orsier (Site Web)
I. Introduction
II. Principe
III. Combinaison de Data Builders
Remerciements
I. Introduction
Dans les tests unitaires nous devons construire des objets avec divers paramètres,
et pour éviter les duplications de code, nous utilisons en général le pattern
Factory
pour produire des objets pré-configurés. Cette utilisation du pattern Factory dans les tests s'appelle en fait
le pattern
ObjectMother. L'inconvénient de ObjectMother est qu'il peut conduire à un grand
nombre de méthodes pour produire toutes les variantes d'objets dont nous avons besoin dans les tests,
peut contenir beaucoup de duplications et devenir difficile à maintenir.
Par conséquent ObjectMother est parfois abandonné par des équipes de développement pour un autre pattern
appelé
Test Data Builder.
Dans cet article je montre comment réaliser Test Data Builder en Delphi (mais le code
se transposera facilement en C# ou en Java, et il sera même plus simple dans ces langages).
Enfin, pour mémoriser le pattern, je vous propose le diagramme suivant - sa signification devrait s'éclaircir à la lecture
de l'article !
II. Principe
Supposons que nous ayons une classe TCustomer souvent utilisée dans les tests, qui se présente de la manière suivante :
TCustomer = class
public
constructor Create(const address, name: string );
function ToString: string ;
private
Faddress: string ;
Fname: string ;
end ;
|
C'est bien sûr une classe simpliste utilisée juste pour montrer le principe.
Le pattern Test Data Builder consiste à créer une classe TCustomerBuilder qui sera chargée de produire des instances
de TCustomer correctement initialisée avec les valeurs les plus couramment utilisées dans les tests, et qui pourront être
facilement remplacées si besoin. Voici un exemple de code utilisant TCustomerBuilder :
customer_builder := TCustomerBuilder.Create() ;
customer_default := customer_builder.build();
customer_one_change := customer_builder
.withName(' Toto ' )
.build();
customer_two_changes := customer_builder
.withAddress(' 1 rue des champs ' )
.withName(' Truc ' )
.build();
customer_two_changes.Free;
customer_one_change.Free;
customer_default.Free;
customer_builder.Free ;
|
Le code ci-dessus montre les particularités du pattern :
- utilisation minimale pour avoir une instance directement utilisable, et initialisée avec des valeurs par défaut valables pour une majorité de tests : customer_default.
- utilisations avec surcharge d'une valeur par défaut : customer_one_change.
- chaînage de surchages, pour une plus grande lisibilité du code de test : customer_two_changes.
C'est malheureusement un peu lourd en Delphi, car il faut gérer manuellement la destruction des objets (et pour cela
stocker toutes les instances dans des variables). Par contre en
C# ou Java on écrit simplement :
customer_one_change = new TCustomerBuilder ().WithName (' Hurlu Berlu ' ).build ();
|
Et l'on ne s'occupe plus du tout de l'instance de TCustomerBuilder qui sera supprimée par le ramasse-miettes.
De plus cela présente l'avantage de repartir avec une nouvelle instance de TCustomerBuilder à chaque fois,
et c'est vital
pour que chaque création d'instance de TCustomer soit indépendante de la précédente. Pour arriver à ce résultat en
Delphi, il faudrait à chaque fois créer un nouveau TCustomerBuilder (lourd car il faut le supprimer aussitôt),
ou encore le réinitialiser après chaque utilisation (soit manuellement avec une méthode reset, soit un peu plus
automatiquement dans la méthode build). C'est cette dernière option que j'ai choisie dans mon implémentation :
TCustomerBuilder = class
public
constructor Create;
function build(const reset : boolean = true ) : TCustomer;
function withAddress(const address: string ): TCustomerBuilder;
function withName(const name: string ): TCustomerBuilder;
private
Faddress: string ;
Fname: string ;
end ;
|
 |
Notez l'appel du constructeur Create depuis une méthode, ce qui permet de refaire les initialisations sans réallouer de la mémoire
(c'est une particularité peu connue des constructeurs Delphi).
|
On verra plus loin qu'il n'est pas toujours souhaitable de réinitialiser le builder, et c'est pourquoi j'ai
introduit ce paramètre reset avec une valeur par défaut.
Le reste de l'implémentation de TCustomerBuilder est très direct. Il faut simplement prendre soin
de regrouper toutes les initialisations dans le constructeur, et de bien retourner self dans les functions
with... afin de permettre le chaînage :
constructor TCustomerBuilder.Create;
begin
Faddress := ' 1 rue Bidule ' ;
Fname := ' Test ' ;
end ;
function TCustomerBuilder.withAddress(
const address: string ): TCustomerBuilder;
begin
Faddress := address;
result := self ;
end ;
function TCustomerBuilder.withName(const name: string ): TCustomerBuilder;
begin
Fname := name;
result := self ;
end ;
|
Et pour terminer, voici l'interface de TCustomerBuilder :
TCustomerBuilder = class
public
constructor Create;
function build(const reset : boolean = true ) : TCustomer;
function withAddress(const address: string ): TCustomerBuilder;
function withName(const name: string ): TCustomerBuilder;
private
Faddress: string ;
Fname: string ;
end ;
|
III. Combinaison de Data Builders
Dans l'exemple précédent, l'adresse pour un TCustomer était de type string ;
si à la place j'utilise maintenant un type plus structuré (afin de gérer indépendamment les différents constituants
de l'adresse), je vais pouvoir écrire du code de test comme :
address_builder_Grenoble := TAddressBuilder.Create.WithCity(' Grenoble ' ) ;
customer_builder := TCustomerBuilder.Create ;
customer1 := customer_builder
.withAddress(address_builder_Grenoble)
.withName(' Y. Truc ' )
.withAccountId(123 )
.build();
customer2 := customer_builder
.withAddress(address_builder_Grenoble)
.withName(' F. Truc ' )
.withAccountId(123 )
.build();
customer2.Free;
customer1.Free;
customer_builder.Free ;
address_builder_Grenoble.Free ;
|
Au passage j'ai un peu enrichi la classe TCustomer avec un nouveau champ AccountId - afin d'introduire une
répétition dont je me débarrasserai dans la section suivante.
Pour arriver au code de test ci-dessus, il me faut une classe TAddress :
TAddress = class
public
constructor Create(const street, postalCode, city: string );
function toString: string ;
private
Fstreet, FpostalCode, Fcity: string ;
end ;
|
ainsi qu'un Builder associé :
type TAddressBuilder = class
public
constructor Create;
constructor newAddress;
function build: TAddress;
function WithStreet(const street: string ): TAddressBuilder;
function WithPostalCode(const postalcode: string ): TAddressBuilder;
function WithCity(const city: string ): TAddressBuilder;
private
Fstreet, FpostalCode, Fcity: string ;
end ;
|
 |
J'ai introduit un constructeur newAddress (qui appelle simplement Create - ce dernier doit continuer à
regrouper toutes les initialisations), afin de voir si cela rendait l'écriture un peu plus "fluide". En effet
l'une des idées derrière ce pattern est d'arriver à créer un petit langage pour faciliter l'écriture des
tests. Avec ce constructeur vous pourriez écrire le code ci-dessous. Mais l'intérêt n'est pas évident en
Delphi où nous sommes obligés de stocker le builder dans une variable pour le détruire nous-mêmes plus loin.
Cela réduit la fluidité, par rapport à C# ou Java.
|
address_builder_Grenoble := TAddressBuilder.newAddress.WithCity(' Grenoble ' ) ;
|
En C# ou Java, nous écririons simplement :
customer1 = new TCustomerBuilder ()
.WithAddress (new TAddressBuilder ().WithCity (' Grenoble ' ))
...
.build ();
|
On peut noter dans ces exemples de code de test que le TCustomerBuilder reçoit directement
un TAddressBuilder, et non pas un TAddress. Cela permet d'éviter de mettre des appels à build()
partout dans le code de test.
Pour en revenir à Delphi, où décidément tout cela est un peu plus laborieux, voici les modifications
de TCustomerBuilder pour accepter en paramètre un TAddressBuilder :
function TCustomerBuilder.build: TCustomer;
begin
result := TCustomer.Create(FaddressBuilder.build(), Fname, FaccountId);
end ;
constructor TCustomerBuilder.Create;
begin
FaddressBuilder := TAddressBuilder.newAddress;
Fname := ' Test ' ;
FaccountId := 1 ;
end ;
function TCustomerBuilder.withAddress(
const addressBuilder: TAddressBuilder): TCustomerBuilder;
begin
FaddressBuilder.Free;
FaddressBuilder := addressBuilder;
result := self ;
end ;
|
Il faut surtout faire très attention à ne pas provoquer de fuite mémoire (voir commentaire dans le code ci-dessus).
On peut également remarquer que l'appel du build de TAddressBuilder est maintenant localisé dans TCustomerBuilder.build,
ce qui semble être une "bonne odeur".
Remerciements


Les sources présentées sur cette page sont libres de droits
et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation
constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ©
2010 Bruno Orsier. Aucune reproduction, même partielle, ne peut être
faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc.
sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à
trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.