Cet article a pour but de clarifier l’installation de behat. Il est fait par
et pour les développeurs débutants avec Behat et le
« Behavior-driven Development »
(BDD que l’on peut traduire par « développement conduit par les comportements »),
les curieux et les enthousiastes.
Symfony 2.2 et Behat 2.4 ont été utilisés; Aucun bundle n’a été blessé lors de la rédaction de cet article.
Mis à jour le 21/08/2013 pour symfony 2.3
Dépendances
Liste des dépendances via composer :
//fichier: composer.json
"require-dev": {
...
"behat/behat": "2.4@stable",
"behat/symfony2-extension": "*@stable",
"behat/mink-bundle": "*@stable",
"behat/mink-browserkit-driver": "*@stable",
"behat/mink-zombie-driver": "*@stable"
}
Lancer l’installation :
composer.phar update
Un petit tour d’horizon :
- « behat/behat » Le cœur de Behat, il ne sait rien faire tout seul.
- « behat/gherkin » Parseur de scénario.
- « behat/symfony2-extension » Intégration à Symfony 2, permet de générer et lancer des suites de tests
- « behat/mink-extension » un navigateur internet « headless » se reposant sur des « drivers »; il ne sait rien faire tout seul.
- « behat/mink-browserkit-driver » Un pilote qui effectue les tests (d’autres existent par ex. goutte, sahi etc.)
- « behat/mink-zombie-driver » un autre pilote pour Mink qui permet de tester du javascript
Symfony2-extension
La documentation indique la configuration suivante qui est déjà en place sur symfony >= 2.2.
À la racine du projet, il est nécessaire d’ajouter le fichier de configuration « behat.yml » :
# fichier: behat.yml default: extensions: Behat\Symfony2Extension\Extension: mink_driver: true Behat\MinkExtension\Extension: default_session: 'symfony2' base_url: http://acme.local/
La configuration de zombie.js est volontairement mise de coté pour l’instant.
L’extension symfony2 de Behat ne sait pas fonctionner correctement avec les sous-dossiers, par contre avec cette configuration, les pages symfony2 sont tout de même générées.
Mise en garde :
Seul la session « symfony2″ ne nécessite pas de serveur web.
C’est peut être suffisant pour des tests simples mais une validations des tests d’acceptation en bonne et due forme nécessite plus. Une solution consiste à lancer le serveur intégré à php >= 5.4 pour lancer les tests et simplement avec la commande app/console server:run.
Initialisation
bin/behat --init @AcmeDemoBundle # ou dans le cas d'un bundle installé : bin/behat --init vendors/acme/demo-bundle/Acme/DemoBundle/
Le dossier « Features » et le minimum utile sont créés dans le bundle.
Ajout de Features
Pour ajouter une feature, il suffit de créer un fichier par exemple « Acme/DemoBundle/Features/Demo.feature » avec la description de la fonctionnalité. Selon le contexte on peut l’écrire en anglais pour les projets open-source ou en français pour les projets client.
Par exemple :
Feature: greetings In order to salute As a user I need to see a greeting message Scenario: An admin see a list of websites Given I am on homepage And I am a user When I click the demo link Then I should see Examples: | message | | hello world |
Ou en français :
# language: fr Fonctionnalité: Salutations Dans le but de saluer En tant qu'utilisateur J'ai besoin de voir un message de salutation Scénario: Un utilisateur affiche un message de salutation Étant donné que je suis sur la page d'accueil Et que je suis un utilisateur Quand je clic sur le lien demo Alors je devrais voir Exemples: | message | | bonjour tout le monde |
Pour décrire une fonctionnalité il faut d’abord définir 3 questions majeurs :
- Quoi ?
- Qui ?
- Comment ?
Dans notre exemple les 3 premières lignes répondent à ces questions. Elles ne sont pas traitées par Behat et ne ne servent qu’à comprendre le besoin. C’est la base du BDD.
Voici un autre exemple plus concret :
- Quoi : « Dans le but d’acheter un aspirateur »
- Qui : « En tant que visiteur »
- Comment : « Je dois de payer un aspirateur qui me convient »
Ensuite le scénario permet de décrire assez clairement comment se comporte l’application en indiquant les 3 principaux mots clés : Étant donné (Given) puis Quand (When) et pour finir Alors (Then). D’autres mots clés sont disponibles et visibles grâce à la doc ou aux commandes behat :
bin/behat --story-syntax --lang=fr bin/behat -dl --lang=fr
Il est remarquable que le scénario ne contient pas d’informations au sujet de la présentation pourtant on aurait pu écrire : Alors je devrais voir "<h1 class="hello">bonjour tout le monde</h1>"
, ce serait une erreur car cette fonctionnalité pourrait aussi être vérifiable sur une autre plateforme tel qu’une application native iOS, seule l’implémentation change.
Plus de détails sur la structure des descriptions de fonctionnalités à voir dans la documentation : Gherkin markup.
Le contexte
Une fois que les features sont écrites, la ligne de commande va permettre de mettre en application les scénarios. D’abord de générer le code contextuel permettant d’exécuter les étapes puis ensuite de valider le fonctionnement de l’application en exécutant le scénario.
La commande suivante permet tout ça :
bin/behat @AcmeDemoBundle # ou dans le cas d'un bundle installé : bin/behat vendors/acme/demo-bundle/Acme/DemoBundle/
La base
À chaque fois qu’une étape est inconnue, Behat proposera d’ajouter le nécessaire par exemple :
/**
* @Given /^I am logged in as "([^"]*)" with the password "([^"]*)"$/
*/
public function iAmLoggedInAsWithThePassword($arg1, $arg2)
{
throw new PendingException();
}
On va donc rajouter cette méthode à notre context Acme/DemoBundle/Features/FeatureContext.php
pour réaliser ce qui est demandé :
namespace Acme\DemoBundle\Features;
use Behat\Symfony2Extension\Context\KernelAwareInterface;
use Behat\MinkExtension\Context\MinkContext;
class FeatureContext extends MinkContext implements KernelAwareInterface
{
//...
/**
* @Given /^I am logged in as "([^"]*)" with the password "([^"]*)"$/
*/
public function iAmLoggedInAsWithThePassword($arg1, $arg2)
{
$this->visit('/admin/en/login');
$this->fillField('username', $arg1);
$this->fillField('password', $arg2);
$this->pressButton('_submit');
}
}
À noter que l’on utilise le context Mink qui fera office de client web.
Sub-Contexts
Il est possible de réutiliser de multiples Context sans avoir la limitation de l’héritage simple imposé jusqu’à php 5.3 par exemple je peux ajouter mes définitions dans UnixContext.php :
namespace Acme\DemoBundle\Features;
use Behat\Behat\Context\BehatContext;
class UnixContext extends BehatContext
{
/**
* @When /^I run "([^"]*)"$/
*/
public function iRun($arg1)
{
throw new \PendingException();
}
}
Et ensuite spécifier dans le Context principal les héritages à utiliser :
public function __construct(array $parameters)
{
$this->useContext('unix', new \Acme\DemoBundle\Features\UnixContext($parameters));
}
À voir plus de détails sur le fonctionnement des Contexts
Des fixtures
On peut indiquer à doctrine ce que nécessite la feature par exemple :
Et il y a des aspirateurs:
| robot aspirateur |
| aspirateur à manivelle |
/**
* @Given /^il y a des aspirateurs:$/
*/
public function ilYADesAspirateurs(TableNode $table)
{
$em = $this->kernel->getContainer()->get('doctrine')->getEntityManager();
foreach ($table->getRows() as $row) {
$hoover = new Hoover();
$hoover->setName($row[0]);
$em->persist($hoover);
}
$em->flush();
}
Le test fonctionnel support de la validation du comportement
À chaque fois que la commande behat est lancée, on aura les indicateurs suivants permettant de vérifier la validité des fonctionnalités :
1 scénario (1 non définies)
5 étapes (1 succès, 3 ignorées, 1 non définies)
0m1.31s
Mink propose des méthodes d’assertions par exemple pour valider notre étape écrite plus tôt :
Alors je devrais voir <message>
On pourra rajouter au FeatureContext :
/**
* @Given /^je devrais voir ([^"]*)$/
*/
public function jeDevraisVoir($arg1)
{
$this->assertElementContainsText("h1", $arg1);
}
Test de Javascript grâce à Zombie.js
Zombie.js est un package de node. Pour travailler avec, on fournit du code javascript qui sera traiter par node.
Installation
Se référer à la documentation puis installer la version 1.4.1 qui est supporté par behat :
sudo npm install zombie@1.4.1 --global
Important, exporter le path en ajoutant ceci à votre .bashrc :
export NODE_PATH="/usr/lib/node_modules"
Utilisation de base
//file: assert_zombies_on_google.js
var Browser = require("zombie");
var assert = require("assert");
browser = new Browser();
//affiche un certain nombre d'éléments de debug ainsi que les requêtes effectuées :
browser.debug = true;
browser.visit("http://google.fr/").
then(function() {
assert.equal(browser.text("H1"), "Deferred zombies");
}).
fail(function(error) {
console.log("Oops", error);
});
On utilisera alors la commande node assert_zombies_on_google.js
.
Le driver de Mink facilite l’utilisation de zombie.js dans une application php (behat).
Configuration pour Behat
Avant tout l’extension Behat pour zombie.js nécessite un serveur web comme indiqué plus haut.
Dans les features il faut utiliser l’annotation @javascript
qui va indiquer à Mink que l’on a besoin de valider du javascript. Ensuite dans behat.yml on va spécifier quel « driver » utiliser.
default:
extensions:
Behat\MinkExtension\Extension:
base_url: http://acme.local/
zombie: ~
javascript_session: zombie
Fonctionnement
Dès que behat est configuré avec zombie, Mink l’utilisera directement (par défaut c’est Goutte qui est utilisé). Donc toutes les méthodes de FeatureContext.php utiliseront zombie.js.
Par exemple lorsque l’on exécute $this->visit('/admin');
c’est la méthode Behat\Mink\Driver\ZombieDriver::visit()
qui est lancée et exécute le code node.js suivant :
pointers = [];
browser.visit("{$url}", function(err) {
if (err) {
stream.end(JSON.stringify(err.stack));
} else {
stream.end();
}
});
Utilisation avancée : valeur de retour
Si on veut aller plus loin suivant l’API de zombie.js, on peut tout de même utiliser la méthode evalJS
:
$js = 'browser.visit("http://acme.local/");stream.end();';
$server = $this->getSession('zombie')
->getDriver()
->getServer();
$out = $server->evalJS($js, 'js');
Il existe deux types de valeur de retour : js ou json.
Les deux exemples suivants sont équivalents et affichent par exemple « int(200) » :
var_dump((int) $server->evalJS('stream.end(JSON.stringify(browser.statusCode))'));
var_dump($server->evalJS('browser.statusCode', 'json'));
Utilisation avancée : ajax
Comme sur tous les navigateurs, il faut attendre l’exécution des différentes requêtes : On va indiquer un temps d’attente maximum et une condition :
use Behat\MinkExtension\Context\MinkContext
class PageContext extends MinkContext
{
/**
* @Given /^I should see a tree of pages$/
*/
public function iShouldSeeATreeOfPages()
{
$this->getSession()->wait(2, '(window.jQuery("#tree ul").length > 0)');
$this->assertNumElements(2, "#tree ul li");
}
}
On notera dans cet exemple la variable « window » rendu disponible grâce à zombie ainsi que l’exécution de javascript (on aurait pu écrire window.$("#tree ul").length
).
Utilisation avancée : assertion complexe
Dans certains cas le contexte Mink ne suffit plus et on veut par exemple combiner zombie.js avec assert de nodejs :
use Behat\MinkExtension\Context\MinkContext
use Behat\Mink\Exception\ExpectationException;
class PageContext extends MinkContext
{
/**
* @Given /^I should see a tree of pages$/
*/
public function iShouldSeeATreeOfPages()
{
$js = <<<JS
var assert = require("assert");
try {
assert.equal(browser.text("h1"), "Pages");
browser.clickLink("Navigation");
assert.equal(browser.text("#page-tree-container h4"), "Navigation");
} catch(err) {
stream.end(JSON.stringify(err.toString()));
}
stream.end();
JS;
$out = $this->getSession('zombie')->getDriver()->getServer()->evalJs($js);
if (!empty($out)) {
throw new ExpectationException(json_decode($out), $this->getSession('zombie'));
}
}
}
Intégration à Jenkins
Le cas particulier de test de bundles dans les vendors nécessite de spécifier le chemin pour chaque bundle.
Dans tous les cas il configurer Ant pour qu’il lance la validation :
<target name="behat" description="Validates features">
<exec executable="bin/behat" failonerror="true">
<arg value="--format=junit" />
<arg value="--out=${basedir}/build/junit" />
<arg path="${basedir}/vendor/acme/demo-bundle/Acme/DemoBundle/" />
</exec>
</target>
Ensuite il est nécessaire de vérifier la configuration du job sur jenkins :
« Publier le rapport des résultats des tests JUnit » doit contenir la valeur build/junit/*.xml
pour permettre à jenkins d’afficher le log d’erreur pour phpunit et behat. »
Astuces
Voir le contenu de la page web
Dans le Context, écrire le contenu directement dans un fichier :
protected function logLastResponse()
{
file_put_contents('behat.out.html', $this->getSession()->getPage()->getContent());
}
Intégration au Makefile
Pour simplifier l’execution de behat on peut ajouter les commandes au Makefile :
test-all: test-install test-behat test-clean
test-install:
#installation de la base de test et des fixtures
test-behat:
bin/behat vendor/presta/cms-core-bundle/Presta/CMSCoreBundle/
test-clean:
#suppression de la base ou des fichiers générés
Chargement de fixtures
Par défaut aucune fixture n’est gérée par behat et comme vu dans l’astuce précédente le chargement des fixtures peut être géré un seule fois dans le Makefile.
L’extension Doctrine data fixtures permet de gérer ce chargement avec l’isolement nécessaire pour chaque validation de Feature.