IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Illustration en Delphi du pattern Test Data Builder

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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 !

Image non disponible

II. Principe

Supposons que nous ayons une classe TCustomer souvent utilisée dans les tests, qui se présente de la manière suivante :

 
Sélectionnez
  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 :

 
Sélectionnez
  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 :

  • 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 :

 
Sélectionnez
	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 :

 
Sélectionnez
  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 :

 
Sélectionnez
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 :

 
Sélectionnez
  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 :

 
Sélectionnez
  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 :

 
Sélectionnez
  TAddress = class
  public
    constructor Create(const street, postalCode, city: string);
    function toString: string;
  private
    Fstreet, FpostalCode, Fcity: string;
  end;

ainsi qu'un Builder associé :

 
Sélectionnez
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.

 
Sélectionnez
  address_builder_Grenoble := TAddressBuilder.newAddress.WithCity('Grenoble') ;

En C# ou Java, nous écririons simplement :

 
Sélectionnez
	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 :

 
Sélectionnez
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).

Image non disponible

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

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 ni 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.