Qu'est-ce que Behat ?
Si vous avez déjà demandé "Comment pouvons-nous être sûrs que notre site web fonctionne comme prévu ?", Behat est là pour répondre à cette question. Behat est un outil qui permet d'écrire des tests pour votre application PHP (comme un site web) d'une manière que tout le monde peut comprendre, même si vous n'êtes pas développeur.
Imaginez Behat comme un assistant méticuleux qui peut vérifier automatiquement que votre site web fonctionne correctement en suivant des instructions écrites en langage courant. Par exemple, vous pourriez écrire "Quand je clique sur le bouton d'inscription, alors je devrais voir un formulaire", et Behat va réellement ouvrir un navigateur, cliquer sur ce bouton et vérifier que le formulaire apparaît.
Ce que Behat apporte à votre projet
Confiance et qualité
Une garantie que votre site web fonctionne correctement, même après des modifications, car Behat peut tester automatiquement toutes les fonctionnalités importantes.
Communication améliorée
Un langage commun entre les décideurs, les développeurs et les testeurs, permettant à chacun de comprendre exactement comment l'application doit se comporter.
Flexibilité et adaptabilité
La possibilité de tester n'importe quelle partie de votre application PHP, qu'il s'agisse d'un site Symfony, Laravel, Drupal, ou même d'une API RESTful.
Documentation vivante
Des spécifications qui sont toujours à jour car elles sont constamment vérifiées contre l'application réelle, servant de référence pour les nouvelles fonctionnalités et les corrections.
En résumé, Behat est un outil qui permet d'assurer que votre site web ou votre application PHP fonctionne correctement, tout en améliorant la collaboration entre les équipes techniques et non techniques. Il traduit les besoins métier en tests automatisés qui vérifient continuellement que votre application fait ce qu'elle est censée faire.
Fonctionnement technique
Behat est un framework de test BDD (Behavior-Driven Development) pour PHP qui permet d'exécuter des tests d'acceptation automatisés. Il utilise le langage
Gherkin pour écrire des scénarios de test en langage naturel et les traduit en actions techniques via des "step definitions".
Architecture et composants
Behat s'articule autour de plusieurs composants clés qui travaillent ensemble pour transformer des spécifications en langage naturel en tests exécutables :
- Fichiers .feature : Écrits en Gherkin, ils contiennent les scénarios de test en langage naturel
- Contexts : Classes PHP qui contiennent les "step definitions", c'est-à-dire le code qui traduit chaque étape Gherkin en actions concrètes
- Extensions : Modules complémentaires qui étendent les fonctionnalités de Behat, comme MinkExtension pour tester les applications web
- Configuration : Fichier behat.yml qui définit les suites de test, les contexts et les extensions à utiliser
Configuration et installation
Configuration de base
Le fichier behat.yml définit comment Behat doit exécuter vos tests. Voici un exemple de configuration qui utilise l'extension Mink pour tester des applications web :
# behat.yml
default:
suites:
default:
paths:
- '%paths.base%/features'
contexts:
- FeatureContext
- Behat\MinkExtension\Context\MinkContext
extensions:
Behat\MinkExtension:
base_url: 'http://localhost:8000'
sessions:
default:
goutte: ~
javascript:
selenium2:
browser: chrome
wd_host: "http://localhost:4444/wd/hub"
Pour installer Behat dans votre projet PHP, utilisez Composer :
composer require --dev behat/behat behat/mink-extension behat/mink-goutte-driver behat/mink-selenium2-driver
Exemple de feature file
Les fichiers .feature contiennent les scénarios de test en Gherkin. Voici un exemple simple pour tester l'authentification d'un utilisateur :
# features/login.feature
Feature: Authentification utilisateur
En tant qu'utilisateur inscrit
Je veux pouvoir me connecter à l'application
Afin d'accéder aux fonctionnalités réservées aux membres
Background:
Given je suis sur la page d'accueil
Scenario: Connexion réussie
When je clique sur "Connexion"
And je remplis le champ "email" avec "utilisateur@exemple.fr"
And je remplis le champ "password" avec "motdepasse123"
And je clique sur le bouton "Se connecter"
Then je devrais voir "Bienvenue, utilisateur@exemple.fr"
And je devrais être sur la page "tableau-de-bord"
Scenario: Connexion échouée
When je clique sur "Connexion"
And je remplis le champ "email" avec "utilisateur@exemple.fr"
And je remplis le champ "password" avec "mauvais_mot_de_passe"
And je clique sur le bouton "Se connecter"
Then je devrais voir "Identifiants invalides"
And je devrais être sur la page "connexion"
Context avec les step definitions
Les contexts contiennent le code PHP qui implémente chaque étape décrite dans les fichiers .feature. Voici un exemple de FeatureContext basique :
<?php
// features/bootstrap/FeatureContext.php
use Behat\Behat\Context\Context;
use Behat\Gherkin\Node\PyStringNode;
use Behat\Gherkin\Node\TableNode;
use Behat\MinkExtension\Context\MinkContext;
/**
* Classe de contexte Behat qui définit les step definitions
*/
class FeatureContext extends MinkContext implements Context
{
private $utilisateurs = [];
private $dernierUtilisateurCree;
/**
* Initialise le contexte
*/
public function __construct()
{
// Initialisation du contexte
$this->utilisateurs = [
'utilisateur@exemple.fr' => [
'password' => 'motdepasse123',
'nom' => 'Jean Dupont',
'role' => 'ROLE_USER'
],
'admin@exemple.fr' => [
'password' => 'admin123',
'nom' => 'Admin Test',
'role' => 'ROLE_ADMIN'
]
];
}
/**
* @Given je suis sur la page d'accueil
*/
public function jeSuisSurLaPageDAccueil()
{
$this->visitPath('/');
}
/**
* @When je clique sur :texte
*/
public function jeCliqueSur($texte)
{
$this->clickLink($texte);
}
/**
* @When je remplis le champ :champ avec :valeur
*/
public function jeRemplisLeChampAvec($champ, $valeur)
{
$this->fillField($champ, $valeur);
}
/**
* @When je clique sur le bouton :bouton
*/
public function jeCliqueSurLeBouton($bouton)
{
$this->pressButton($bouton);
}
/**
* @Then je devrais voir :texte
*/
public function jeDevraisVoir($texte)
{
$this->assertPageContainsText($texte);
}
/**
* @Then je devrais être sur la page :page
*/
public function jeDevraisEtreSurLaPage($page)
{
$this->assertUrlRegExp('#/' . $page . '(\\?.*)?$#');
}
/**
* @Given il existe un utilisateur avec l'email :email et le mot de passe :password
*/
public function ilExisteUnUtilisateurAvecLEmailEtLeMotDePasse($email, $password)
{
$this->utilisateurs[$email] = [
'password' => $password,
'nom' => 'Utilisateur Test',
'role' => 'ROLE_USER'
];
// Dans un contexte réel, on créerait l'utilisateur dans la base de données
// ou via une API
}
/**
* @Given les produits suivants existent:
*/
public function lesProduitsExistent(TableNode $tableNode)
{
$produits = $tableNode->getHash();
foreach ($produits as $produit) {
// Dans un contexte réel, on ajouterait ces produits dans la base de données
// Exemple :
// $this->entityManager->getRepository('App:Produit')->create($produit);
}
}
/**
* @Then le panier devrait contenir :nombre produits
*/
public function lePanierDevraitContenirProduits($nombre)
{
// Vérification du nombre de produits dans le panier
$this->assertElementContainsText('.cart-count', $nombre);
}
/**
* @Then le total du panier devrait être :montant
*/
public function leTotalDuPanierDevraitEtre($montant)
{
// Vérification du montant total du panier
$this->assertElementContainsText('.cart-total', $montant);
}
}
Intégration avec Symfony
Behat s'intègre très bien avec les frameworks PHP comme Symfony. Voici un exemple de context qui utilise les services Symfony :
<?php
// features/bootstrap/SymfonyContext.php
use Behat\Behat\Context\Context;
use Behat\Symfony2Extension\Context\KernelAwareContext;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Behat\MinkExtension\Context\MinkContext;
use Doctrine\ORM\EntityManagerInterface;
use App\Entity\User;
use App\Entity\Product;
class SymfonyContext extends MinkContext implements Context, KernelAwareContext
{
private $kernel;
private $entityManager;
private $container;
private $currentUser;
/**
* @BeforeScenario
*/
public function clearData()
{
// Nettoyer la base de données avant chaque scénario
// (ceci est un exemple, à adapter selon votre architecture)
if ($this->entityManager) {
$connection = $this->entityManager->getConnection();
$connection->executeQuery('DELETE FROM product');
$connection->executeQuery('DELETE FROM user WHERE email LIKE "%test%"');
}
}
/**
* Définit l'instance du kernel de l'application.
*/
public function setKernel(KernelInterface $kernel)
{
$this->kernel = $kernel;
$this->container = $kernel->getContainer();
$this->entityManager = $this->container->get('doctrine')->getManager();
}
/**
* @Given j'ai une base de données propre
*/
public function jAiUneBaseDeDonneesPropre()
{
// Assurez-vous que les tables sont vides ou contiennent uniquement des données de test
// Cette étape est souvent utilisée dans les scénarios qui ont besoin d'un état initial connu
}
/**
* @Given il existe un utilisateur avec le rôle :role
*/
public function ilExisteUnUtilisateurAvecLeRole($role)
{
$user = new User();
$user->setEmail('user_test_' . uniqid() . '@exemple.fr');
$user->setPassword(password_hash('password123', PASSWORD_BCRYPT));
$user->setRoles([$role]);
$user->setName('User Test');
$this->entityManager->persist($user);
$this->entityManager->flush();
$this->currentUser = $user;
}
/**
* @Given je suis connecté en tant qu'administrateur
*/
public function jeSuisConnecteEnTantQueAdministrateur()
{
// Créer un utilisateur admin si nécessaire
if (!$this->currentUser || !in_array('ROLE_ADMIN', $this->currentUser->getRoles())) {
$this->ilExisteUnUtilisateurAvecLeRole('ROLE_ADMIN');
}
// Connecter cet utilisateur dans la session Symfony
$session = $this->container->get('session');
$token = new UsernamePasswordToken(
$this->currentUser,
null,
'main',
$this->currentUser->getRoles()
);
$this->container->get('security.token_storage')->setToken($token);
$session->set('_security_main', serialize($token));
$session->save();
// Définir un cookie de session dans le navigateur Mink
$cookie = $session->getName() . '=' . $session->getId();
$this->getSession()->setCookie($session->getName(), $session->getId());
// Visiter la page pour s'assurer que la session est chargée
$this->visitPath('/');
}
/**
* @Given les produits suivants existent dans le catalogue:
*/
public function lesProduitsExistentDansLeCatalogue(TableNode $table)
{
foreach ($table->getHash() as $data) {
$product = new Product();
$product->setName($data['nom']);
$product->setPrice((float) $data['prix']);
$product->setDescription($data['description'] ?? '');
$product->setStock((int) ($data['stock'] ?? 10));
$this->entityManager->persist($product);
}
$this->entityManager->flush();
}
/**
* @When je vais sur la page de gestion des produits
*/
public function jeVaisSurLaPageDeGestionDesProduits()
{
$this->visitPath('/admin/products');
}
/**
* @Then je devrais voir :count produits listés
*/
public function jeDevraisVoirProduitsListes($count)
{
$elements = $this->getSession()->getPage()->findAll('css', '.product-item');
assertEquals((int) $count, count($elements), "Le nombre de produits affichés ne correspond pas");
}
/**
* @When j'ajoute un nouveau produit avec:
*/
public function jAjouteUnNouveauProduitAvec(TableNode $table)
{
$this->visitPath('/admin/products/new');
$data = $table->getRowsHash();
foreach ($data as $field => $value) {
$this->fillField($field, $value);
}
$this->pressButton('Enregistrer');
}
/**
* @Then le produit :productName devrait exister dans la base de données
*/
public function leProduitDevraitExisterDansLaBaseDeDonnees($productName)
{
$product = $this->entityManager->getRepository(Product::class)
->findOneBy(['name' => $productName]);
assertNotNull($product, "Le produit '$productName' n'existe pas dans la base de données");
}
}
Fonctionnalités principales
- Test d'application web - Grâce à Mink, Behat peut naviguer sur votre site, cliquer sur des liens, remplir des formulaires et vérifier le contenu des pages
- Plusieurs drivers - Support pour divers navigateurs via Selenium, ainsi que des drivers headless comme Goutte pour des tests plus rapides
- Hooks - Points d'extension comme @BeforeScenario, @AfterFeature qui permettent d'exécuter du code à des moments précis du cycle de test
- Tags - Possibilité de marquer des features ou scénarios avec des tags (@javascript, @api, etc.) pour les filtrer lors de l'exécution
- Profils - Définition de plusieurs configurations pour exécuter les tests dans différents environnements (dev, CI, etc.)
- Transformations - Conversion automatique des paramètres dans les step definitions pour simplifier le code
Extensions populaires
- MinkExtension - Pour tester des applications web en contrôlant un navigateur
- Symfony2Extension - Intégration avec Symfony pour accéder au conteneur de services et au kernel
- ApiExtension - Pour tester des API REST facilement
- DoctrineExtension - Facilite l'accès à Doctrine ORM dans vos tests
- PageObjectExtension - Implémente le pattern Page Object pour des tests web plus maintenables
Bonnes pratiques
- Organisez vos contexts - Créez plusieurs classes de context, chacune responsable d'un domaine spécifique (WebContext, ApiContext, DatabaseContext, etc.)
- Utilisez les backgrounds - Pour éviter la répétition dans vos scénarios
- Isolez vos tests - Chaque scénario doit pouvoir s'exécuter indépendamment des autres
- Utilisez des tags - Pour catégoriser vos scénarios et les exécuter sélectivement
- Suivez les principes BDD - Concentrez-vous sur le comportement attendu, pas sur les détails d'implémentation
- Maintenez un état propre - Utilisez les hooks @BeforeScenario et @AfterScenario pour préparer et nettoyer l'environnement
Behat excelle dans la création de tests d'acceptation automatisés pour des applications PHP, en permettant une description claire des comportements attendus et une vérification continue que ces comportements sont correctement implémentés.
Cas d'usage
Sites e-commerce Symfony
Tests automatisés des parcours d'achat complets, de la navigation dans le catalogue jusqu'au paiement, en passant par l'ajout au panier et la saisie des informations de livraison.
CMS Drupal personnalisés
Vérification que les workflows de publication de contenu, les permissions utilisateurs et les formulaires personnalisés fonctionnent correctement après des mises à jour du core ou des modules.
APIs RESTful
Tests d'intégration des API, en vérifiant les réponses, les codes d'état HTTP et les structures de données retournées par différents endpoints dans diverses conditions.
Applications SaaS
Vérification des processus d'inscription, de gestion de compte, de facturation et des fonctionnalités spécifiques à chaque niveau d'abonnement dans des applications complexes.
Intranets d'entreprise
Tests des workflows métier complexes, des rapports personnalisés et des interactions entre différents modules ou systèmes intégrés dans les applications internes.
Systèmes legacy
Création de tests d'acceptation pour des systèmes existants sans tests, avant de procéder à des refactorisations majeures ou des migrations technologiques.
Behat dans les pipelines CI/CD
Behat est souvent intégré dans les workflows d'intégration continue pour automatiser les tests à chaque commit ou pull request. Voici un exemple de configuration pour GitHub Actions :
name: Behat Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
behat:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:5.7
env:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: test_db
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
selenium:
image: selenium/standalone-chrome:latest
ports:
- 4444:4444
options: --health-cmd="/opt/bin/check-grid.sh" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
extensions: mbstring, xml, ctype, iconv, mysql, pdo_mysql
coverage: none
- name: Install dependencies
run: composer install --prefer-dist --no-progress
- name: Start web server
run: php -S localhost:8000 -t public &
- name: Run Behat tests
run: vendor/bin/behat --format=progress