Utiliser Behat et Mink dans une application Laravel 4

Fan de développement web élégant basé sur le framework PHP Laravel, et féru de Behavior-Driven Development à grands coups de Behat et Mink ? Si tel est le cas, il est possible que vous ayez rencontré quelques difficultés à faire cohabiter les deux via Composer, notamment avec la quatrième et dernière version en date du framework. Il n'est pourtant pas vraiment en cause ; je pense pouvoir affirmer sans trop me tromper que gérer ces deux outils de tests fonctionnels en tant que dépendances est un beau bordel actuellement, entre la version 2.5 "stable" et la version 3.0 encore-en-beta-mais-qui-sort-quand-même-par-défaut-un-peu-trop-facilement, mais passons.

Dans mon cas, la première difficulté rencontrée a donc principalement été liée à des conflits de dépendances. Qu'à cela ne tienne, ce paquet Composer, sobrement intitulé behat-laravel, m'a été fort utile pour pouvoir tout installer simplement. Voici un fichier composer.json rudimentaire avec les packages employés :

{
"require": {
"laravel/framework": "4.1.*",
"phpunit/phpunit": "3.8.*@dev",
"guilhermeguitte/behat-laravel": "dev-master",
"behat/mink": "1.5@stable",
"behat/mink-extension": "*",
"behat/mink-selenium2-driver": "*"
},

...
}

Une fois le tout prêt à l'emploi, on commence donc à écrire ses premiers tests pour vite se rendre compte que l'écosystème global de Laravel, très complet, s'avère difficilement dispensable lorsqu'on souhaite n'utiliser que certaines parties du framework. Pour cet exemple qui sent bon le vécu, imaginons que nous souhaitions tester un formulaire de connexion et que nous voulions donc pouvoir créer un utilisateur en base de données histoire d'avoir une paire d'identifiants valides à tester. Voici le fichier .feature correspondant :

Feature: Log in to the app
In order to be able to access my account
I need to be able to fill in a login form with my credentials

Background:
Given I am on "/"

Scenario: Type in a valid account's credentials
Given "[email protected]" has an account with "prout" as the password
When I fill in "Adresse e-mail" with "[email protected]"
And I fill in "Mot de passe" with "prout"
And I press "Connexion"
Then I should be on "/account"

Scenario: Type in a invalid account's credentials
When I fill in "Adresse e-mail" with "[email protected]"
And I fill in "Mot de passe" with "nothing"
And I press "Connexion"
Then I should be on "/"
And I should see "Votre identification a échoué, veuillez réessayer"

Scenario: Submit the form without filling it
When I press "Connexion"
Then I should be on "/"
And I should see "Adresse e-mail : ce champ est requis"
And I should see "Mot de passe : ce champ est requis"

Scenario: Submit the form with an invalid e-mail address in
When I fill in "Adresse e-mail" with "jesuisinvalidelol"
And I press "Connexion"
Then I should be on "/"
And I should see "Adresse e-mail : ce champ est invalide"

Le vocabulaire de base de Mink couvrira à lui seul l'essentiel de ces instructions. Nous allons simplement devoir donner du sens à l'expression Given /^"([^"]*)" has an account with "([^"]*)" as the password$/ :

<?php

use Behat\Behat\Exception\PendingException;
use Behat\MinkExtension\Context\MinkContext;

class LoginContext extends MinkContext
{
/**
* @Given /^"([^"]*)" has an account with "([^"]*)" as the password$/
*/
public function hasAnAccountWithAsThePassword($email, $password)
{
$user = new User();
$user->name = 'toto';
$user->email = $email;
$user->setPassword($password);
$user->save();
}
}

Enfantin, n'est-il pas ? Oui, sauf que dans les faits :

  • On n'a pas accès à la base de données
  • La classe User hérite d'Eloquent, qui est un alias d'une classe namespacée, accessible uniquement dans le contexte d'une application Laravel
  • Même topo pour Hash, qui de plus doit être initialisé avec un provider de hash (de mon temps, on appelait ça un dealer)

Je pourrais vous guider pas à pas, d'erreur d'exécution en erreur d'exécution, à travers les méandres de mon parcours sur le chemin tortueux menant à la résolution du problème exposé ici, mais je doute que ce soit vraiment intéressant. Voyons donc directement les modifications apportées à cette classe de contexte pour faire tourner le bazar :

<?php

use Behat\Behat\Exception\PendingException;
use Behat\MinkExtension\Context\MinkContext;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\AliasLoader;
use Illuminate\Hashing\HashServiceProvider;
use Illuminate\Support\Facades\Facade;
use Illuminate\Database\Capsule;

class LoginContext extends MinkContext
{
public function __construct()
{
// On instancie une app bidon
$app = new Application();

// On lui évite de faire appel à des propriétés indéfinies
$app->bind(
'path.storage',
function () {
return '';
}
);

// On instancie un fournisseur de chiffrement (belle francisation, tiens)
$hash = new HashServiceProvider($app);
$hash->register();

// On utilise notre fausse app
Facade::setFacadeApplication($app);

// On charge les alias existants dans une instance de la classe qui va bien
$appConfig = require(dirname(dirname(dirname(__DIR__))).'/config/app.php');
$this->aliasLoader = AliasLoader::getInstance($appConfig['aliases']);

// On récupère les infos de connexion à la base de données de test
$dbConfig = require(dirname(dirname(dirname(__DIR__))).'/config/database.php');
$this->db = new Capsule\Manager();
$this->db->addConnection($dbConfig['connections']['sqlite_test']);
}



/**
* @Given /^"([^"]*)" has an account with "([^"]*)" as the password$/
*/
public function hasAnAccountWithAsThePassword($email, $password)
{
// On charge les alias dont on va avoir besoin
$this->aliasLoader->load('Eloquent');
$this->aliasLoader->load('Hash');

// On boot Eloquent avec notre instance de la base de données
$this->db->bootEloquent();

// On peut exécuter des requêtes arbitraires comme ceci :
$this->db->getConnection()->delete('DELETE FROM users');

$user = new User();
$user->name = 'toto';
$user->email = $email;
$user->setPassword($password);
$user->save();

// On est bien \o/
}
}

Voilà qui devrait vous donner une meilleure idée du type de bidouille (car c'est bien ce dont il s'agit) envisageable pour pouvoir utiliser une partie de l'environnement de Laravel sans démarrer complètement l'application. Je suis bien évidemment preneur de toute solution plus solide, n'hésitez pas à laisser un petit mot doux au bas de cet article si le coeur vous en dit !