Tester une API stateless avec Behat
Publié le 23 août 2016
Introduction #
L’état est à la base des scénarios Behat. Tout d’abord, vous déterminez l’état du système sous test (SUT) avec les étapes « Given » (Étant donné que). Ensuite, vous continuez à manipuler votre système à travers les étapes « When » (Quand) (toujours en modifiant l’état du système sous test) et, pour finir, vous testez l’état final à travers les étapes « Then » (Alors).
Lorsque vous testez un système comme une application monopage ou un site web à états, l’état final d’un scénario est géré par le système sous test (que ce soit par le navigateur, la session php ou autre).
Mais lorsque vous testez un système sans état tel qu’une API, par définition, l’état final n’est plus géré par le système sous test. Cela nous laisse la responsabilité de gérer l’état final dans les classes Context.
Solutions #
Il existe des solutions pour gérer l’état du scénario. La façon la plus communément admise de gérer cette problématique est la suivante : en combinant le fait que les contextes sont créés pour chaque scénario (c’est à dire que les instances Context ne sont pas les mêmes d’un scénario à l’autre) et qu’ils sont des classes php toutes simples. On peut facilement assigner des propriétés de classe avec « $this », et chaque méthode du contexte peut désormais accéder à la même propriété dans la classe Context. Sans compter que vous pouvez facilement injecter une instance Context dans une autre (voir http://docs.behat.org/en/v2.5/guides/4.context.html#using-subcontexts).
Cela fonctionne plutôt bien, mais il y a quelques inconvénients dérangeants :
- C’est quelque peu verbeux, vous devez injecter des contextes, écrire des setters, des getters…
- Ici, il y a un couplage entre deux contextes qui brouille leurs responsabilités.
- Vous (le développeur) **devez** savoir quel contexte gère l’état dont vous avez besoin.
- Les méthodes des étapes ne déclarent pas leurs dépendances à un fragment de l’état précédent.
Le couplage induit par cette méthode est quelque peu insidieux car il commence dans les fichiers sources Gherkin !
Imaginons ces deux scénarios :
Scénario: Je crée un nouveau post
Étant donné que l’opérateur est authentifié comme superadmin
Et que l’opérateur crée un nouveau post
Alors ce nouveau post devrait être publiquement visible
Ici, les deux étapes importantes sont : « Étant donné que l’opérateur est authentifié comme superadmin » et « Étant donné que l’opérateur est un cron ». Elles contribuent toutes deux à l’état en fournissant aux étapes suivantes un opérateur qui devrait être capable de créer un nouveau post. Les autres étapes sont exactement les mêmes et partagent de ce fait la même méthode de classe Context. Si toutes les étapes sont écrites dans la même classe Context, pas de problème. Assignez tout simplement l’opérateur précédemment instancié à une propriété de classe « $operator ».
Et nous pouvons facilement imaginer une méthode Context comme celle-ci :
/**
@When l’opérateur crée un nouveau post
*/
public function createANewPost()
{
$operator = $this->operator;
$postManager->createPost($operator, ‘THIS IS SOME POST CONTENT’);
}
Mais si vous souhaitez avoir 3 contextes spécifiques :
- WebContext, dont la responsabilité est de gérer l’authentification dans un navigateur ;
- CliContext, dont la responsabilité est de gérer l’authentification depuis une console CLI ;
- PostContext, dont la responsabilité est de gérer la manipulation des posts.
WebContext et CliContext doivent donner l’instance de l’objet contenant l’opérateur au PostContext, probablement à travers un setter.
PostContext doit être injecté dans WebContext et CliContext afin qu’ils puissent appeler la méthode PostContext::setOperator().
Oui… Bon... Ça fonctionne. Mais cela risque de devenir pénible lorsque vous aurez besoin d’autres contextes qui nécessitent un opérateur. Vous devrez injecter chaque nouveau contexte à la fois dans WebContext et CliContext. Chaque nouveau contexte aura probablement besoin d’implémenter une sorte d’OperatorAwareInterface avec une méthode « setOperator(Operator $operator) » (car vous ne voulez pas que CliContext et WebContext sachent quelle méthode appeler dans chaque contexte), etc.
Bien entendu, vous pourriez ajouter une sorte de contexte « data » dont la responsabilité serait de stocker les données de l’état. Et vous savez quoi ? Nous l’avons fait pour vous sous la forme d’une extension Behat !
A la découverte de l’extension BehatScenarioState #
Et si pendant leur exécution, les étapes pouvaient fournir une partie de l’état du scénario à un service de stockage puis injecter des fragments de l’étape aux étapes suivantes directement en tant que paramètres de méthode ? Pour cela, trois de nos développeurs, Rodrigue Villetard, Vincent Chalamon et Hamza Amrouche ont créé l’extension ScenarioStateBehatExtension.
Note : Si vous souhaitez des instructions plus précises à propos de l’installation et de l’utilisation de cette extension, nous vous recommandons de lire le fichier readme.
Cette extension Behat va permettre aux étapes du scénario de fournir et de consommer ce que nous appellerons des fragments de l’état final. Chaque scénario obtient son propre état, unique et isolé.
Imaginons une fonctionnalité comme celle-ci :
Fonctionnalité : Un singe ramasse des bananes
Scénario: Le singe donne une banane à un autre singe
Quand le bonobo prend une banane
Et que le bonobo donne cette banane au « gorille »
Vous voyez le **cette** banane ? Ce que nous voulons pendant l’exécution de la deuxième étape, c’est une référence à la banane précise que le bonobo a initialement prise. Cette extension Behat va nous aider à propager la référence de la banane à travers les étapes.
Vous pouvez fournir le fragment d’état grâce à la méthode « ScenarioStateInterface::provideStateFragment(string $key, mixed $value) ».
/**
* @When le bonobo prend une banane
*/
public function takeBanana()
{
$banana = ’Yammy Banana’;
$bonobo = new Bonobo(’Gerard’);// Ici, la banane « Yammy Banana » est partagée aux étapes suivantes à travers la clef « scenarioBanana »
$this->scenarioState->provideStateFragment(’scenarioBanana’, $banana);
// Ici, le bonobo Gerard est partagé aux étapes suivantes à travers la clef « scenarioBonobo »
$this->scenarioState->provideStateFragment(’scenarioBonobo’, $bonobo);
}
Pour consommer les fragments d’état fournis à l’état du scénario, vous devez ajouter les arguments nécessaires aux méthodes de l’étape en utilisant l’annotation « @ScenarioStateArgument », avec le nom du fragment d’état dont cette étape a besoin.
use GorghoaScenarioStateBehatExtensionAnnotationScenarioStateArgument;
/**
* @When le bonobo donne cette banane à : singe
*
* @ScenarioStateArgument(“scenarioBanana”)
* @ScenarioStateArgument(name=”scenarioBonobo”, argument=”bonobo”)
*
* @param string $monkey
* @param string $scenarioBanana
* @param Bonobo $bonobo
*/
public function giveBananaToGorilla
(
$monkey,
$scenarioBanana,
Bonobo $bonobo
)
{
// note: assertEquals are purely fictive functions here
assertEquals($monkey, 'gorilla');
assertEquals($scenarioBanana, 'Yammy Banana');
assertEquals($bonobo->getName(), 'Gerard');
}
Les avantages de cette méthode sont multiples :
- Les dépendances d’état des étapes ne sont plus associées à des contextes spécifiques ;
- Vous bénéficiez du type hinting php pour les arguments des méthodes ;
- Les tests échouent rapidement ;
- C’est une méthode concise.
Conclusion #
Si vous lisez attentivement la partie installation, vous voyez peut-être le drapeau « @RC » sur la commande « composer require ».
Nous sommes confiants que l’extension est à présent assez stable. Mais avant de publier une version 1.0.0 officielle, nous aimerions avoir davantage de feedback de la communauté :
- L’idée/la conception de cette extension Behat a-t-elle du sens pour vous ?
- Cette extension vous semble-t-elle assez ergonomique pour votre codage quotidien avec Behat ?
- Peut-être avez vous déjà trouvé un moyen plus élégant de résoudre ce problème. Si oui, lequel ?
N’hésitez pas à venir nous en parler et à contribuer à ce projet : il est sous licence MIT !