Behat, qu’est ce que c’est ?
Behat est un framework utilisé pour du “Behavior Driven Development” (BDD). Il permet de tester des scénarios rédigés dans un langage simple et naturel pour l’homme, qu’il transforme ensuite en tests applicatifs.
Pour exécuter les tests ainsi rédigés, Behat exécute un autre programme chargé de simuler le comportement d’un utilisateur : un émulateur. Il en existe un grand nombre : Goutte, Selenium, etc. et chacun a ses spécificités. Voici les caractéristiques des plus utilisées afin de vous aider à choisir :
Selenium2 | Zombie | Goutte | |
Supporte le JS | Oui | Oui | Non |
Redimensionnement des fenêtres | Oui | Non | Non |
Manipulation des cookies | Oui | Oui | Oui |
Gestion des iframes | Oui | Non | Non |
Gestion des formulaires | Oui | Oui | Oui |
Accès aux entêtes des requêtes | Non | Oui | Oui |
À première vue, on peut voir que Selenium2 permet de tout faire mais il a aussi des limites : voici la documentation permettant d’avoir une vision plus complète : http://mink.behat.org/en/latest/guides/drivers.html
Installation
Nous avons choisi d’installer Behat en utilisant composer, il suffit de lancer la commande :
composer require behat/behat
À ce moment, composer installera toutes les dépendances nécessaires à Behat (voir la documentation officielle). Sur Drupal 8, nous avons préféré installer Behat à côté du dossier « web », ce qui permet de le distinguer des dossiers Drupal.
Une fois que composer a fini de s’exécuter, il faut alors procéder à l’initialisation de Behat en exécutant behat --init
depuis le répertoire où Behat est installé.
À ce moment on voit l’apparition de deux fichiers très importants :
- behat.yml : permettant de configurer Behat en lui ajoutant toutes les extensions souhaitées
- FeatureContext.php : permettant d’ajouter des tests personnalisés
Configuration du behat.yml :
Il devrait se décomposer de la manière suivante :
- Les contextes : Permet de rajouter des classes et paramètres nécessaires au projet
- Les formatters : L’affichage du résultat
- Les extensions : Ajout d’extensions comme celle de Drupal, ou celle permettant de faire des captures d’écran…
default:
gherkin:
filters:
tags: ~@wip #rajouter @wip aux sénarios en cours de développement, ils seront ignorés. Pour les executer volontairement, lancer la commande `behat --tags=wip`
suites:
default:
contexts:
- FeatureContext
- DrupalDrupalExtensionContextDrupalContext
- DrupalDrupalExtensionContextMinkContext
- DrupalDrupalExtensionContextMessageContext
- DrupalDrupalExtensionContextDrushContext
formatters:
progress: #Ajout de progress pour ne pas voir le détail des étapes de chaque scénario sur le terminal. Pointillés à la place
output_path: null
pretty: #Ajout de pretty pour voir le détail des étapes de chaque scénario sur le terminal
output_path: null
html:
output_path: %paths.base%/build/html/behat
extensions:
BexBehatScreenshotExtension:
image_drivers:
local:
screenshot_directory: "LIEN/DU/DOSSIER/POUR/LES/SCREENSHOTS"
clear_screenshot_directory: true # Enable removing all images before each test run. It is false by default.
BehatMinkExtension:
base_url: URLDELAVM
goutte: ~
default_session: selenium2
javascript_session: selenium2
browser_name: 'chrome'
selenium2:
wd_host: "http://localhost:8643/wd/hub"
DrupalDrupalEx tension:
blackbox: ~
api_driver: 'drupal'
drupal:
drupal_root: 'DOSSIER_CONTENANT_LE_DRUPAL'
api_driver: 'drush'
drush:
root: 'DOSSIER_OU_DRUSH_EST_INSTALLE'
emuseBehatHTMLFormatterBehatHTMLFormatterExtension:
name: html
renderer: Twig,Behat2
file_name: index
print_args: true
print_outp: true
loop_break: true
Il faut aussi ajouter quelques modifications pour avoir un développement plus simple (pour d’autres ajouts, voici un lien qui devrait vous aider http://docs.behat.org/en/v2.5/guides/7.config.html) :
- Ajout du tag
@wip
permettant d’ignorer tous les tests ayant ce tag lors de l’exécution des tests - Ajout des screenshots à chaque test non réussi
- Choix de la forme d’affichage des tests (détaillée ou non) dans la section formatters
- Ajout de PhantomJS supportant mieux le JS que selenium 2 (il faut cependant lancer le serveur PhantomJS après l’avoir installé sur la VM) voici les commandes pour le faire :
cd /opt
wget
https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2
tar -xjvf phantomjs-2.1.1-linux-x86_64.tar.bz2
mv phantomjs-2.1.1-linux-x86_64 phantomjs
ln -s /opt/phantomjs/bin/phantomjs /usr/bin/phantomjs
which phantomjs
phantomjs --webdriver=8643
La partie configuration s’arrête là pour le moment. Passons au coeur du sujet : les tests !
Tests
Les tests se créent selon l’arborescence suivante (si vous devez faire comme moi : création d’un dossier Behat à côté du dossier web) : behat/features.
Dans ce dossier, il n’y aura que les tests mais écrits de deux façons :
- La façon Behat : le langage utilisé par l’homme naturellement (fichiers : *****.feature)
- La fonction cachée derrière la phrase utilisée (fichiers : ****.php).
Le lien qui relie ses deux tests se situent dans le commentaire inséré au dessus de la fonction dans le fichier ****.php. Cela permet de transférer la transcription humaine du test en langage PHP interprétable pour notre machine.
Petite astuce : certaines fonctions sont déjà intégrées dans le module de Drupal. Par conséquent nous pouvons lister toutes les fonctions grâce à la commande : behat -dl
. Cela liste toutes les fonctions déjà disponibles sous le langage de Behat que nous pouvons réutiliser.
Par exemple, voyons ce test simple footer.feature :
Feature: Footer
Scénario : Vérification de la présence du footer sur la homepage
Given I am on the homepage
Then I should see an ".footer" element
C’est assez simple à lire non ? Tout d’abord on exprime à Behat que l’on veut aller sur la homepage (configuration nécessaire du Behat.yml pour savoir sur quelle adresse il doit aller) pour vérifier ensuite la présence de la classe ‘footer’
.
Lorsqu’on lance ce test dans un terminal, voici ce que l’on obtient :
Passons à une fonction plus compliquée nécessitant un développement personnel d’une fonction : la gestion des cookies.
Feature: Utilisation des cookies
Background: Connection admin établie
Given I am on the homepage
Scenario: Je n'ai pas le cookie => message
When I have not this cookie "g-cookies"
Then I should see an ".cookie-banner-exist" element
Scenario: Acceptation du bandeau cookie
Given I have not this cookie "g-cookies"
When I should see an ".cookie-banner-exist" element
And I follow "Accepter"
Then I have this cookie "g-cookies"
And I reload the page
And I have this cookie "g-cookies"
And I should not see an ".cookie-banner-exist" form element
Ici nous voyons deux scénarios dans le fichier .feature. J’ai volontairement mis ces deux scénarios dans le même fichier afin de leur définir un contexte commun. On peut voir au dessus des scénarios la présence d’un ‘Background’. Il permet de définir un contexte à tous les scénarios qui se situent dans le fichier. Cela évite donc pas mal de duplications de code.
Dans un deuxième temps, sur la phrase Given I have not this cookie "g-cookies"
il y a eu un développement personnel. C’est-à-dire que l’on a dû développer une fonction permettant de vérifier la présence de ce cookie. La voici :
/**
*
* @When /^I have this cookie "(?P(?:[^"]|")*)"$/
*/
public function IHaveThisCookie($cookie) {
if ($this->getSession()->getCookie($cookie) == NULL) {
throw new Exception();
}
}
Il s’agit d’une fonction simple en POO (Programmation Orientée Objet), mais qui est bien utile. Cette fonction a en paramètre “$cookie”. Ce paramètre est renseigné via “(?P<text>(?:[^"]|")*)
”, nous avons donc le lien entre Behat et les fonctions associées.
Cette fonction s’ajoute au fichier FeatureContext.php afin d’être reconnue par Behat pour qu’elle soit utilisée.
Ainsi nous pouvons ajouter toutes les fonctions souhaitées les unes à la suite des autres (avec des commentaires) pour pouvoir effectuer des tests appropriés.
Voici une liste des fonctions permettant de vérifier pas mal de choses utiles :
- L’attente d’une réponse AJAX
/**
* @Then /^I wait for the ajax response$/
*/
public function iWaitForTheAjaxResponse()
{
$this->getSession()->wait(5000, '(0 === jQuery.active)');
}
- Remplir un fichier WYSIWYG dans Drupal :
/**
* Fills in WYSIWYG editor with specified id.
*
* @Given /^(?:|I )fill in "(?P<text>[^"]*)" in WYSIWYG editor "(?P<iframe>[^"]*)"$/
*/
public function iFillInInWYSIWYGEditor($text, $iframe) {
try {
$this->getSession()->switchToIFrame($iframe);
}
catch (Exception $e) {
throw new Exception(sprintf("No iframe with id '%s' found on the page '%s'.", $iframe, $this->getSession()->getCurrentUrl()));
}
$this->getSession()->executeScript("document.body.innerHTML = '<p>".$text."</p>'");
$this->getSession()->switchToIFrame();
}
- La suppression de chaque noeud créé pour les tests par Behat :
/**
* Remove any created nodes.
*
* @AfterScenario
*/
public function cleanNodes() {
// Remove any nodes that were created.
foreach ($this->nodes as $node) {
$this->getDriver()->nodeDelete($node);
}
$this->nodes = array();
}
- L’affichage du code HTML de la page s’il y a une erreur dans les tests :
/**
*
* @AfterScenario
*/
public function printLastResponse()
{
echo (
$this->getSession()->getCurrentUrl()."nn".
$this->getSession()->getPage()->getContent()
);
}
En résumé les tests fonctionnels avec Drupal 8 nous permettent de vérifier, à tout instant, ce qui a été prévu avec le client fonctionnel. Ce qui permet au développeur de vite relever les problèmes lors de l’exécution de ces tests (à chaque push sur une branche particulière, par exemple) et de les corriger. Malgré une documentation assez fluide et lisible, la réalisation de ces tests est assez chronophage… Il faut compter en moyenne 20% de temps de développement en plus sur une tâche pour y ajouter des tests. Mais ces 20% de temps supplémentaires sont en général bénéfiques pour la suite du projet. Plus un projet est grand, plus le besoin de tests se fait sentir.
Crédit photo : David Travis