Un meilleur job mieux payé ?

Deviens chef de projet, développeur, ingénieur, informaticien

Mets à jour ton profil pro

ça m'intéresse

Developpez.com - ALM
X

Choisissez d'abord la catégorieensuite la rubrique :


Illustration en Delphi du pattern Test Data Builder

Date de publication : 21 avril 2010

Par Bruno Orsier (Site Web)
 


       Version PDF (Miroir)   Version hors-ligne (Miroir)
Viadeo Twitter Facebook Share on Google+        



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 en Factory pour produire des objets pré-configurés. Cette utilisation du pattern Factory dans les tests s'appelle en fait le pattern en 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é en 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();

  //do something with the instances

  customer_two_changes.Free;
  customer_one_change.Free;
  customer_default.Free;

  customer_builder.Free ;
Le code ci-dessus montre les particularités du pattern :

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;
warning 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();

    // do something

  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;
idea 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; // fuite de mémoire sinon !
  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

Merci à Johan Martinsson qui m'a fait découvrir le livre Growing Object-Oriented Software, Guided by Tests, et ses nombreux exemples en Java dont je me suis inspiré pour cet article (en particulier le chapitre 22 qui est consacré à ce pattern).



               Version PDF (Miroir)   Version hors-ligne (Miroir)

Valid XHTML 1.0 TransitionalValid CSS!

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.

Contacter le responsable de la rubrique ALM