Behat – créer des tests solides et efficaces

{lang: 'fr'}

Ca commence à faire déjà quelques temps que j’utilise Behat, et j’ai eu la chance de pouvoir l’utiliser sur différents projets, gros et petits. Je commence donc à avoir un peu plus de recul sur la chose, et à avoir accumulé pas mal de mauvaises pratiques.

Par manque de chance (ou pas ^^), il a fallu que je commence à utiliser Behat sur un projet assez important. C’est donc là que j’ai commencé à faire mes plus grosses erreurs. Je vous propose de voir quelles ont été ces erreurs, et comment éviter de les reproduire.

Utiliser Behat sans faire du Développement piloté par le comportement

C’est évident, mais il faut le dire : Behat n’est qu’un outil, ce qui compte vraiment c’est le Développement piloté par le comportement. Et oui ! Behat n’est ni un outil de test, ni un outil de spécification. Ce n’est « rien de plus » qu’un bonus par rapport à une démarche de travail.

Si on prend Behat, comme il m’est arrivé de le faire, comme simple outil de test, on fonce dans le mur : au bout de plusieurs mois on risque d’avoir substitué Behat à des outils de tests unitaires (PHPUnit, atoum). Behat permet (entre autres) de tester du besoin fonctionnel. Encore faut-il que ce besoin soit exprimé !

Faire du refractoring de code plutôt que du refractoring de phrases

Allez, créons une méthode privée réutilisable dans notre Contexte de définition. Ah, et puis passons la publique le jour où il nous faudra l’utiliser dans un autre Contexte…

Au final on se retrouve avec une application dans l’application. Quand c’est le cas, et ça m’est arrivé, c’est qu’on confond refractoring de code et refractoring de définitions.

Les Contextes de définition servent à traduire des expressions en code source. C’est donc sur ces expressions qu’il faut se focaliser. L’idéal est de réussi à parvenir à découper chaque expression en unités atomiques fondamentales.

Par exemple :

Quand que j'ajoute dans mon panier "Télévision Sony" depuis le catalogue produits

On peut découper cette expression en sous-expressions (étapes)

Etant donné que je suis sur la page du catalogue produit
Quand j'ajoute "Télévision Sony" au panier

Et au final on peut arriver à des expressions atomiques simples :

Etant donné que je suis sur "/catalogue/produits"
Quand je sélectionne "Télévision Sony"
Et je clique "Ajouter au panier"

Vous voyez que là on fait du refractoring de phrases. L’avantage est que c’est hyper simple avec Behat :

<?php
use Behat\Behat\Context\Step;

//

/**
* @Given /^que je consulte le catalogue produit$/
*/
public function queJeConsulteLeCatalogueProduit()
{
	return array(
	    new Step\Given('que je suis sur la page "/catalogue/produits"')
	);
}

etc.

Il suffit d’utiliser des sous-appels d’étapes dans la définitions.

C’est la seule manière que j’ai trouvé pour se retrouver avec des Contextes de définition simples et surtout stables dans le temps. Sinon on passe notre temps à les réécrire dans leur ensemble.

Par contre ça oblige à avoir plein de définitions, d’où le point suivant :

Utiliser Behat sans couche d’isolation

Au départ, quand on utilise Behat, on a tendance à se créer 2 ou 3 contextes de définition, et puis c’est tout. C’est à mon avis une erreur. Aujourd’hui, je démarre chaque projet qui utilise Behat avec au moins ce découpage :

  • Contextes de définition métiers
  • Contexte de définition de vue (web)
  • Couche d’isolation pour la persistance des données si besoin

Pourquoi ? Tout simplement parce que je me suis rendu compte d’une chose : il arrive souvent de devoir repasser sur des définitions ! Et oui. Et c’est long, laborieux et, disons-le, très très démotivant.

Il faut au moins regrouper les définitions qui concernent l’interface graphique dans un contexte spécifique. Par défaut, si vous utilisez Mink, c’est ce qui fait plus ou moins implicitement lorsque vous utilisez des définitions toute-prêtes de Mink.

Mais que se passera t-il quand le client vous annoncera qu’il sort une appli mobile ? Normalement, appli mobile ou web, aucune spécification ne devrait changer. Par contre, si vous avez mal organisés vos Contextes de définition, vous allez galérer.

Au contraire, si vous regroupez tout ce qui concerne l’UI dans un Contexte de vue, mettons par exemple « View\WebContext.php », il vous suffit simplement de gérer vos sous-contextes :

public function __construct(array $parameters)
{
    if ($parameters['view'] == 'mobile') {
        $this->useContext('view', new View\MobileContext($parameters));
    } else {
        $this->useContext('view', new View\WebContext($parameters));
    }
}

C’est aussi simple que ça : il vous suffit de regrouper tout ce qui concerne l’affichage dans un Contexte spécifique.

Bien entendu, pour les très grosses applications on peut aller plus loin, et d’ailleurs ça devient très vite intéressant : créer un Contexte par page. Par exemple:

$this->useContext('view.catalogue', new View\Web\CatalogueContext($parameters));
$this->useContext('view.panier', new View\Web\PanierContext($parameters));

Ca peut paraître extrémiste, mais dans le cas où votre application est conséquente, c’est très utile. Il faut bien se dire que tout ce travail consiste à solidifier votre architecture de test. De la même manière qu’il faut découper une application en blocs de code (modules), il faut découper les Contextes de définition selon leur… contexte.

Après, il faut prendre la juste mesure des choses, et trouver un découpage adapté :

  • aux besoins
  • à la taille du projet
  • au temps disponible
  • à la compétence de l’équipe

Exactement comme on le ferait pour un applicatif.

Voilà pour ces quelques retours que je souhaitais partager avec vous. Après, ils sont basés sur mon expérience personnelle, et je serai très curieux de savoir ce que vous en pensez, et surtout comment vous vous avez fait pour gérer la croissance de vos projets utilisant Behat / Cucumber / etc.. N’hésitez pas à partager votre expérience…

6 réflexions au sujet de « Behat – créer des tests solides et efficaces »

  1. Serge HARDY

    Salut,

    Le fait d’écrire des phrases du type:

    Etant donné que je suis sur « /catalogue/produits »

    n’est-t-il pas dangereux au bout d’un certain moment car là on part implicitement du principe que l’on est dans un contexte web?

    Ne devrait-on pas s’abstraire de la notion du url (le client s’en moque au fond que ça soit du web), et écrire des choses du style:

    Etant donné que je suis sur la liste des produits?

    Ce qui implique sans doute de gérer en dessous du mapping avec des constantes de classes, etc…

    D’autre part, n’a-t-on par intérêt à tester unitairement les contextes?

    Dernière question: comment gérer l’hyper-dépendance des scénario avec la vue, les traductions, le DOM, etc… Je ne trouve pas beaucoup de bonnes pratiques à ce sujet alors que ça semble assez évident que ça devient vite un problème!

    1. Jean-François Lépine Auteur de l’article

      Je ne suis pas très partisan de modifier les commentaires. Ou alors indique moi exactement quelle phrase corriger s’il te plait

      > comment gérer l’hyper-dépendance des scénario avec la vue, les traductions, le DOM, etc… Je ne trouve pas beaucoup de bonnes pratiques à ce sujet alors que ça semble assez évident que ça devient vite un problème!

      C’est justement pour gérer l’hyper-dépendance des scénarios avec la vue, etc. qu’il faut à mon avis créer des couches d’isolation, comme je le présente ici pour la couche Vue. Dans tous les cas, au final on se retrouve forcément avec une couche directement liée à une interface graphique, donc non, ce n’est pas gênant d’avoir « Etant donné que je suis sur « /catalogue/produits » » du moment que cette phrase est écrite dans (dans mon exemple) dans la classe ‘View\WebContext’.

      > D’autre part, n’a-t-on par intérêt à tester unitairement les contextes?

      C’est une bonne question, à laquelle j’aurai tendance à répondre tout simplement : non :) . Tout est une question de mesure entre :
      - une architecture trop compliquée
      - une confiance a minima dans un code source
      - un temps de développement infini

  2. Cyrano

    « Encore faut-il que ce besoin soit exprimé »
    Je crois bien qu’on insistera jamais assez sur ce point. Je bute dessus tous les jours depuis des années et je n’ai à ce jour rencontré personne en mesure de m’indiquer de piste vers un début de solution.

    Le besoin doit en principe être exprimé par l’utilisateur final, après tout, c’est tout de même son outil qu’on construit. Mais voilà, s’il est vrai qu’il a l’habitude de ses processus de travail, l’outil dont il a besoin pour faciliter sa tâche devrait théoriquement impliquer qu’il ralentit un peu le rythme pour analyser ses propres processus pour en noter tous les détails, et ses descriptions permettront d’identifier les différents contextes et résultats attendus qui pourront ensuite être traduits en code.

    Pour ma part, j’ai découvert l’existence de Behat largement trop tard par rapport au projet et je ne me lancerai pas dans l’implémentation de tests maintenant. Mais j’irai plus loin en disant qu’à première vue (et ce n’est qu’une impression personnelle), ça prendrait des proportions considérables : mettre en place des tests en atomisant en plein de petits tests correspondant au différents paramètres dont il faut tenir compte pour déterminer si le résultat obtenu est bon ou non reviendrait quasiment à écrire l’application en double.

    « je serai très curieux de savoir [...] comment vous vous avez fait pour gérer la croissance de vos projets »

    Pour ma part, et sans Behat, il y a au départ le problème de la base de données : les données sont très atomisées et mes tables ont en fin de compte assez peu de colonnes. Pas de redondance et une très grande souplesse qui me permet de la faire grandir par des ajouts ponctuels sans rien casser. En contre-partie, je peux me retrouver à devoir faire certaines requêtes avec un nombre appréciable de jointures, mais c’est à mon sens un moindre mal.
    Quant à l’application elle-même, je suis parti sur un principe de découpage en modules par domaines fonctionnels pratiquement indépendants les uns des autres. Mais bon, c’est une approche toute personnelle, et je dois confesser que j’ai fait ça davantage « à l’instinct » plutôt que sur la base de quelques leçons d’architecture logicielle que je n’ai de toutes façon jamais suivies. Mais je dis « pratiquement indépendants » et je reconnais que ce n’est pas toujours vrai dans la pratique. Au fil des évolutions, des ajustements fonctionnels sur un domaine ont pu avoir des impacts sur d’autres domaines. Je m’efforce d’éviter la méthode « La Rache », mais des fois, ça peut y ressembler furieusement.

    Là où les problèmes peuvent apparaitre, c’est lorsque l’utilisateur m’annonce la bouche en cœur que finalement son besoin fonctionnel est différent : en réalité, ce qui a été fait était conforme à la demande initiale, mais il manquait des éléments… et du coup, les contextes changent…. et le code doit changer. Personnellement, c’est cette partie là que je trouve particulièrement démotivante, parce que j’ai une sainte horreur de devoir refaire un truc qui marchait parce que le demandeur n’a finalement pas fait correctement son propre boulot.

    En conclusion, je serais tenté de penser que si l’utilisateur n’est pas impliqué à fond dans le processus de développement, on aura beau utiliser toutes les sortes de tests qu’on voudra, on aboutira toujours à des applications qui ne feront pas exactement ce qui correspond réellement au besoin.

    My 2¢

  3. Julien

    Je ne suis pas du tout convaincu …

    Peut être que ceux qui ont déjà compris ce que tu explique sont rassurés mais pour ma part je ne comprends pas vers quoi tu souhaites nous emmener.

    J’utilises des tests Behat tous les jours et j’ai même fait le forcing pendant plusieurs semaines pour que leur mise en place soit effective mais je ne comprends pas là tout de suite ce qu’il ne faut pas faire :/

    PS : Tu devrais ajouter un plugin permettant de s’inscrire aux commentaires du post par email :)

    1. Jean-François Lépine Auteur de l’article

      Mais c’est tout à fait ton droit :)

      Je n’ai sans doute pas réussi à exprimer mon message clairement. Par exemple, quand tu organise tes Contextes de définition, comment gère-tu lorsque l’interface graphique change ? Par exemple, pour un cas extrême, tu avais une application web, là où désormais tu as une appli mobile ? Ou dans une moindre mesure, lorsque la page est refondue ? Ou comment gère-tu lorsque le besoin initialement exprimé change (ce qui est normal avec le temps) ?

      Si les contextes ne sont pas organisés, ce genre de modification nécessite de repasser à plein d’endroits dans les Contextes de définition. L’utilisation d’une couche de Vue, qui va centraliser ces définitions, permet à mon avis de n’avoir à faire ces modifications que dans un seul endroit.

      Il en va de même pour le refractoring de phrases : si le découpage en expressions atomiques n’est pas fait, un changement dans l’expression de besoin initiale risque d’avoir des répercussions importantes dans les Contextes de définition, nécessitant de repasser à pas mal d’endroits.

      Bref, mon objectif est de trouver des solutions pour faciliter l’évolutivité des Contextes de définition face à un changement de Spécification ou d’Interface.

      PS: bonne idée, je vais chercher ça

Les commentaires sont fermés.