BDD, Behat, Gherkin – Introduction au Behavior Driven Development avec Behat et Mink

Le développement piloté par le comportement, voilà la traduction forcement moins attirante que sa définition première en anglais : « Behavior Driven Development ». Plus encore le mot comportement est sans doute plus sexy que le mot test. Pour qui s’agite à l’audition des mots Agile ou Lean, vous avez déjà du avoir les oreilles qui bourdonnent à des mots tel que : Behat, Gherkin pour ne citer que ces 2 termes.
Si vous vous intéressez au développement que ce soit mobile ou web, la nécessité d’intégrer des tests fonctionnels via Behat est quasi une obligation. Dans le cas par exemple du développement en PHP, sous les frameworks Symfony ou Laravel, cela vous sera systématiquement demandé.

Un extrait plutôt censé du blog de Dan North sur l’importance du behaviour-driven development (BDD)

It has evolved out of established agile practices and is designed to make them more accessible and effective for teams new to agile software delivery. Over time, BDD has grown to encompass the wider picture of agile analysis and automated acceptance testing.

Ce qui est extrêmement intéressant c’est que vous forgez une approche transverse et un « langage » commun avec l’ensemble des parties prenantes de votre projet de développement : le client (product-owner), la direction de projet, l’équipe de développement. Bien évidemment, les « user stories » peuvent être rédigées de la même manière. Un des points essentiels avec l’utilisation du Gherkin, c’est qu’il peut vous aider à faire toute la lumière sur ce que vous considérez comme implicite afin de le rendre explicite. Le BDD est aussi une manifestation pratique dans le management agile, instituant l’implication de chacun donc la cohésion du groupe, au même titre que les traditionnels rituels agile.

Dan North donne un exemple très parlant de cette exhaustivité lié à l’usage du Gherkin pour décrire des user stories sur un exemple aussi banal que le retrait d’argent à un distributeur avec une carte. L’usage du Gherkin en décomposant cette simple action en une suite logique permet de ne rien omettre sur les conditions de bonne ou de mauvaise exécution.

L’exemple en langage commun

+Title: Customer withdraws cash+
As a customer, I want to withdraw cash from an ATM, so that I don’t have to wait in line at the bank.

En utilisant le modele « given-when-then », la complexité éventuelle d’une action aussi anodine s’éclaire tour à coup.

+Scenario 1: Account is in credit+
Given the account is in credit And the card is valid And the dispenser contains cash When the customer requests cash Then ensure the account is debited And ensure cash is dispensed And ensure the card is returned

A noter que l’usage intensif de la conjonction « and » permet de connecter plusieurs « givens » ou plusieurs résultats d’une manière intuitive.

+Scenario 2: Account is overdrawn past the overdraft limit+
Given the account is overdrawn And the card is valid When the customer requests cash Then ensure a rejection message is displayed And ensure cash is not dispensed And ensure the card is returned

Source : https://dannorth.net/introducing-bdd/

Mink Behat & the tests

Laissons tomber le volet théorique, vous s’attelez à la pratique. L’ensemble des exemples ci-dessous ont été menés dans l’environnement suivant : Firefox 47.0, une version de PHP 5.5.38 avec l’aide de la console.
Vous aurez besoin de homebrew, d’installer composer sir ce n’est pas déjà fait. Il y a eu quelques difficultés agaçantes que vous aurez peut-être à résoudre éventuellement.

1. Problème de timezone dans PHP
Il y avait des erreurs de date lors de l’exécution des tests, sans gravité mais troublant. Le problème venait du fait que la « timezone » n’était pas définie dans le php.ini.

Les commandes de base pour localiser et modifier le php.ini

php -v # check la version 
php --ini # localise le fichier php.ini
vi /usr/local/etc/php/5.5/php.ini # editer le fichier php.ini

Extrait du php.ini à modifier

;;;;;;;;;;;;;;;;;;;
; Module Settings ;
;;;;;;;;;;;;;;;;;;;
 
[CLI Server]
; Whether the CLI web server uses ANSI color coding in its terminal output.
cli_server.color = On
 
[Date]
; Defines the default timezone used by the date functions
; http://php.net/date.timezone
;date.timezone =
date.timezone = Europe/Paris

2. Installer geckodriver
Autre souci, plus ennuyeux, pour installer geckodriver, le plus simple est de passer par Homebrew avec la commande suivante.
brew install geckodriver

3. La commande pour lancer pour lancer le serveur selenium si vous souhaitez faire des tests dans un navigateur firefox
java -jar /chemin-vers-le-fichier/selenium-server-standalone-3.0.0-beta3.jar
Source : Selenium Standalone Server http://goo.gl/EUxR76

La marche à suivre

La première chose à faire consiste créer un répertoire dans lequel vous allez effectuer vos travaux. Le notre se nomme demo-2
Pour vous faciliter la tache, voilà une video youtube très didactique, qui a servi de base à ce billet.

La source du fichier composer.json qui vous permettra d’installer les éléments nécessaires aux tests dans votre répertoire. Notre projet se nomme demo-2. cd /chemin-vers-votre-projet-behat/demo-2/ puis composer install

	{
	    "require": {
	        "behat/mink": "1.4@stable",
	        "behat/mink-goutte-driver": "*",
	        "behat/mink-selenium2-driver": "*",
	        "behat/behat": "2.4@stable",
	        "behat/mink-extension": "*",
	        "guzzle/guzzle": "3.5",
	        "moodlehq/behat-extension": "1.*"
	    },
	    "minimum-stability": "dev",
	    "config": 
	        {
	            "bin-dir": "bin/"
	        }
	}

Une fois le tout installer, voilà les principales commandes dont vous aurez besoin. Toutes ces commandes sont à passer dans le répertoire du projet demo-2.

php bin/behat --init # créer le nécessaire pour behat
php bin/behat # pour lancer vos tests avec behat
php bin/behat -dl # pour connaitre toutes les règles de mink pour ecrire vos .feature

Le fichier search.feature pour tester la recherche du site Le monde

	# Content of file renting.feature
 
	Feature: Search
	  In  order to find a word
	  As website user
	  I am able to search for a word
	  # avec le @javascript vous lancez le test dans le navigateur firefox
	  # sans le @javascript vous faites passer le test en mode headless.
		@javascript
		  Scenario: Search for a word that exists
		    Given I am on "http://www.lemonde.fr/"
		When I fill in "recherche_globale" with "ONU"
		And I press "rechercher"
		Then I should see "la situation à Alep"

L’installation des éléments via la composer
BDD, Behat, Gherkin - Introduction au Behavior Driven Development avec Behat et Mink

Le test en mode headless
BDD, Behat, Gherkin - Introduction au Behavior Driven Development avec Behat et Mink

L’inspecteur de Chrome vous permet de connaitre les éléments id|name|label|value à placer dans votre scénario de test.
BDD, Behat, Gherkin - Introduction au Behavior Driven Development avec Behat et Mink

Quelques commandes pour faire une sortie sous forme de rapport html des résultats de vos tests

php bin/behat  --format html --out report.html
php bin/behat -f pretty,progress,junit --out ,progress.out,xml

La sortie complète des actions avec la commande php bin/behat -dl

	 When /^I click "([^"]*)"$/
	Given /^I wait for (\d+) seconds$/
	 When I wait for :arg1 seconds
	Given /^I wait for "([^"]*)" seconds$/
	Given /^(?:|I )am on (?:|the )homepage$/
	 When /^(?:|I )go to (?:|the )homepage$/
	Given /^(?:|I )am on "(?P<page>[^"]+)"$/
	 When /^(?:|I )go to "(?P<page>[^"]+)"$/
	 When /^(?:|I )reload the page$/
	 When /^(?:|I )move backward one page$/
	 When /^(?:|I )move forward one page$/
	 When /^(?:|I )press "(?P<button>(?:[^"]|\\")*)"$/
	 When /^(?:|I )follow "(?P<link>(?:[^"]|\\")*)"$/
	 When /^(?:|I )fill in "(?P<field>(?:[^"]|\\")*)" with "(?P<value>(?:[^"]|\\")*)"$/
	 When /^(?:|I )fill in "(?P<value>(?:[^"]|\\")*)" for "(?P<field>(?:[^"]|\\")*)"$/
	 When /^(?:|I )fill in the following:$/
	 When /^(?:|I )select "(?P<option>(?:[^"]|\\")*)" from "(?P<select>(?:[^"]|\\")*)"$/
	 When /^(?:|I )additionally select "(?P<option>(?:[^"]|\\")*)" from "(?P<select>(?:[^"]|\\")*)"$/
	 When /^(?:|I )check "(?P<option>(?:[^"]|\\")*)"$/
	 When /^(?:|I )uncheck "(?P<option>(?:[^"]|\\")*)"$/
	 When /^(?:|I )attach the file "(?P[^"]*)" to "(?P<field>(?:[^"]|\\")*)"$/
	 Then /^(?:|I )should be on "(?P<page>[^"]+)"$/
	 Then /^the (?i)url(?-i) should match (?P<pattern>"([^"]|\\")*")$/
	 Then /^the response status code should be (?P<code>\d+)$/
	 Then /^the response status code should not be (?P<code>\d+)$/
	 Then /^(?:|I )should see "(?P<text>(?:[^"]|\\")*)"$/
	 Then /^(?:|I )should not see "(?P<text>(?:[^"]|\\")*)"$/
	 Then /^(?:|I )should see text matching (?P<pattern>"(?:[^"]|\\")*")$/
	 Then /^(?:|I )should not see text matching (?P<pattern>"(?:[^"]|\\")*")$/
	 Then /^the response should contain "(?P<text>(?:[^"]|\\")*)"$/
	 Then /^the response should not contain "(?P<text>(?:[^"]|\\")*)"$/
	 Then /^(?:|I )should see "(?P<text>(?:[^"]|\\")*)" in the "(?P<element>[^"]*)" element$/
	 Then /^(?:|I )should not see "(?P<text>(?:[^"]|\\")*)" in the "(?P<element>[^"]*)" element$/
	 Then /^the "(?P<element>[^"]*)" element should contain "(?P<value>(?:[^"]|\\")*)"$/
	 Then /^the "(?P<element>[^"]*)" element should not contain "(?P<value>(?:[^"]|\\")*)"$/
	 Then /^(?:|I )should see an? "(?P<element>[^"]*)" element$/
	 Then /^(?:|I )should not see an? "(?P<element>[^"]*)" element$/
	 Then /^the "(?P<field>(?:[^"]|\\")*)" field should contain "(?P<value>(?:[^"]|\\")*)"$/
	 Then /^the "(?P<field>(?:[^"]|\\")*)" field should not contain "(?P<value>(?:[^"]|\\")*)"$/
	 Then /^the "(?P<checkbox>(?:[^"]|\\")*)" checkbox should be checked$/
	 Then /^the "(?P<checkbox>(?:[^"]|\\")*)" checkbox should not be checked$/
	 Then /^(?:|I )should see (?P<num>\d+) "(?P<element>[^"]*)" elements?$/
	 Then /^print last response$/
	 Then /^show last response$/

En savoir plus