Logo Behat

Behat

Le framework de tests BDD pour Icône PHPPHP qui permet d'écrire des scénarios de test en langage naturel avec Icône GherkinGherkin et de les exécuter automatiquement sur votre application.

Pour les non-initiés

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.

Pour les développeurs

Fonctionnement technique

Behat est un framework de test BDD (Behavior-Driven Development) pour Icône PHPPHP qui permet d'exécuter des tests d'acceptation automatisés. Il utilise le langage Icône GherkinGherkin 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 :

Exemple de fichier behat.yml
# 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
# 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 :

features/bootstrap/FeatureContext.php
<?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 :

features/bootstrap/SymfonyContext.php
<?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.

Applications concrètes

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