<!--
  canonical-url: https://www.avonture.be/fr/blog/behat-introduction
  published:     2024-06-24
  This is a static plain-Markdown mirror generated at build time.
  Visit the canonical URL above for the fully rendered page, with images and interactive components.
-->

# Introduction à Behat

> Apprenez à utiliser Behat, le framework open source de Behavior-Driven Development (BDD) pour PHP. Cette introduction couvre l'écriture de tests fonctionnels puissants en langage naturel et la mise en place de votre premier projet.

---
![Introduction à Behat](/img/v2/functional_tests.webp)

**TL;DR**

Ce tutoriel présente le Behavior-Driven Development (BDD) en PHP avec Behat et Docker. Il couvre la mise en place d'un environnement de test avec Chrome et Mink, l'écriture de scénarios Gherkin et l'implémentation des définitions d'étapes pour automatiser les tests fonctionnels d'applications web.

> Lisez aussi mon article sur [Cypress](/blog/cypress)

Au travail, je fais partie d'une équipe de développeurs PHP qui construit ensemble une très grosse application propriétaire avec une interface web.

Cette application est développée avec le framework Laravel et nous utilisons [PHPUnit](https://phpunit.de/index.html) et [Pest](https://pestphp.com/) pour exécuter nos tests unitaires (voir [Write PHP unit tests using Pest](/blog/pest_tips)).

Mais en plus des tests unitaires, nous mettons aussi en place un outil qui simule des actions sur notre interface : accéder à la page de login, saisir un identifiant et un mot de passe, simuler le clic sur le bouton « login », attendre que notre écran principal s'affiche puis, sur la nouvelle page, vérifier un tas de choses / effectuer un tas de tâches.

C'est un peu comme demander à un humain de rejouer des scénarios encore et encore, chaque jour, pour s'assurer que nous n'avons pas introduit de régression dans nos derniers développements — par exemple une belle nouvelle fonctionnalité dont les changements de code ont cassé une fonctionnalité existante.

Pour cela, nous utilisons [Behat](https://docs.behat.org/en/latest/). C'est une dépendance PHP [composer](https://getcomposer.org/) que vous pouvez ajouter à votre projet (ou à un nouveau). L'idée est de pouvoir écrire des assertions en anglais (ou en français) courant, du genre « je vais sur le site ABC puis je clique sur l'entrée de menu blog et je devrais recevoir une liste d'articles » ou « dans la zone de recherche, je tape Docker puis je clique sur le bouton de recherche et je devrais recevoir des articles portant le tag Docker ».

Dans cet article, nous allons en apprendre plus sur Behat et voir comment l'utiliser pour un premier test.

Comme on peut le lire sur le site de [Behat](https://docs.behat.org/en/latest/), *Behat est un framework open source de Behavior-Driven Development pour PHP. C'est un outil qui vous aide à livrer un logiciel qui compte, grâce à une communication continue, une découverte délibérée et l'automatisation des tests.* Cela veut dire que vous pouvez commencer à écrire vos scénarios avant même de coder. Vous pouvez demander à votre client d'écrire ses assertions en langage courant. Ensuite, le développeur se met à coder et, une fois terminé, il lui suffit d'exécuter les scénarios écrits pour s'assurer que le logiciel répond aux exigences du client.

## Introduction au BDD - Behavior-Driven Development {#introduction-to-bdd---behavior-driven-development}

Imaginez que vous êtes un client et que vous demandez à un développeur de créer un nouveau site web. Dans votre cahier des charges, vous lui écrivez des choses comme :

* En tant que visiteur, je voudrais une entrée de menu qui, une fois cliquée, va (faire ceci) ;
* En tant que visiteur, je veux pouvoir accéder à un moteur de recherche qui me permet de faire une sélection dans une catégorie d'articles puis, dans cette catégorie, de lancer une recherche sur le mot (un mot). Une liste d'articles sur ce thème s'affiche alors ;
* En tant que gestionnaire du site, je veux pouvoir me connecter à une interface d'administration et, après un login réussi, je dois voir les options (liste des fonctionnalités) ;
* En tant que gestionnaire du site, je dois pouvoir ajouter un nouvel article où je dois spécifier un titre, une catégorie, une image principale et un texte. Une fois enregistré, je dois voir le nouvel article dans la liste des articles et, en triant la liste sur la date/heure de création, il doit être le premier de la liste ;
* (et beaucoup plus)

En termes Behat, ces phrases s'appellent des scénarios. Vous pouvez les écrire avant même que le site existe. Et pendant les étapes de codage, le développeur exécutera vos scénarios de temps en temps. Au premier lancement, tous les scénarios vont échouer (le site n'existe pas ; les fonctions demandées ne sont pas encore développées). Le développeur travaillera sur l'une ou l'autre fonctionnalité ; il relancera les tests BDD et, au fil du projet, les scénarios passeront au vert jusqu'à la fin du projet, quand tous les scénarios seront réussis et que le projet pourra vous être envoyé pour recette.

## Résultat {#result}

Un scénario est juste un fichier `.feature` écrit en anglais courant, une ligne par étape :

**Blog.feature**

```feature
Feature: Clicking on the Blog menu item should give me the list of articles

  Scenario: I click on the Blog menu
    Given I am on "https://www.avonture.be"
    Then I click on the "Blog" menu item
    Then I should be on "/blog"
```

**Ce scénario utilise trois étapes :**

1. `Given I am on "https://www.avonture.be"`

2. `Then I click on the "Blog" menu item` et

3. `Then I should be on "/blog"`.

Avant d'écrire une seule ligne de PHP, demandez simplement à Behat de l'exécuter — il comprend déjà le
scénario et nous dit exactement ce qui manque encore :

![Premier lancement](./images/first_run.webp)

C'est tout l'argument : des étapes en langage courant, analysées et suivies par Behat, et du PHP uniquement pour
faire en sorte que chaque étape *fasse* réellement quelque chose. Le reste de cet article construit ce PHP.

## Mettre en place le projet {#set-up-the-project}

Pour cet article, nous allons créer un nouveau projet Behat et valider ce scénario : **« En tant que visiteur, je veux pouvoir visiter le site XYZ ; cliquer sur un élément de navigation appelé "Blog". Le site m'affiche alors la liste des articles de blog. »**

Comme vous le savez si vous lisez régulièrement mon blog, je bois, mange et respire Docker, ce qui veut dire que je n'ai plus ni PHP, ni composer, ni Apache sur mon ordinateur.

Donc, pour notre nouveau défi ci-dessus (que j'accepte avec plaisir), je vais créer un petit projet avec Docker, PHP 8.2 et composer.

C'est parti...

* D'abord, créons un nouveau répertoire : `mkdir /tmp/behat && cd $_`.
* Là, créons un fichier appelé `Dockerfile` avec le contenu ci-dessous. Ce script est déjà gros mais, comme ça, nous aurons tout ce qu'il faut dès maintenant.

**Dockerfile**

```docker
# cspell:disable
# The user ID and name and group ID of the user to create in our image
ARG DOCKER_UID=1000
ARG DOCKER_GID=1000
ARG USERNAME=johndoe

# The version of Chrome we'll install
ARG CHROME_VERSION=115.0.5763.0

# This is a PHP project
FROM php:8.2-fpm

# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends git jq zip unzip wget gnupg \
    && apt-get clean \
    && rm -rf /tmp/* /var/lib/apt/lists/*

# We'll create a new user with the same uid/gid than ours, on our host machine.
ARG DOCKER_UID
ARG DOCKER_GID
ARG USERNAME

RUN groupadd --gid ${DOCKER_GID} "${USERNAME}" \
    && useradd --home-dir /home/"${USERNAME}" --create-home --uid ${DOCKER_UID} \
        --gid ${DOCKER_GID} --shell /bin/sh --skel /dev/null "${USERNAME}"

# Download and install Chrome
ARG CHROME_VERSION
ARG DOWNLOAD_URL="https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json"

# Download Chrome (the browser and the driver)
RUN wget --no-check-certificate --no-verbose -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
    && sh -c 'echo "deb https://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
    && apt-get update -yqq \
    && apt-get install -y --no-install-recommends -yq gconf-service libasound2 libatk1.0-0 libc6 libcairo2 \
        libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libgconf-2-4 \
        libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 \
        libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 \
        libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 \
        libxss1 libxtst6 ca-certificates fonts-liberation libnss3 lsb-release \
        xdg-utils \
    && apt-get clean \
    && rm -rf /tmp/* /var/lib/apt/lists/* \
    # --- Download the browser ---
    && ZIP_URL=$(curl $DOWNLOAD_URL | jq -r ".versions[] | select(.version==\""$CHROME_VERSION"\").downloads.chrome[] | select(.platform==\""linux64"\") .url") \
    && wget --no-check-certificate --no-verbose -O /tmp/chrome_browser.zip $ZIP_URL \
    && printf "\e[0;105m%s\e[0;0m\n" "Using chromedriver $(/usr/local/bin/chrome/chromedriver --version)" \
    #  Unzip and create the /usr/local/bin/chromedriver executable  (-j means don't create a subfolder with the name of the archive; unzip in the folder directly)
    && unzip -j /tmp/chrome_browser.zip -d /usr/local/bin/chrome \
    && rm -f /tmp/chrome_browser.zip \
    && ls -alh /usr/local/bin/chrome \
    && chmod +x /usr/local/bin/chrome/chrome \
    # --- Download the driver ---
    && ZIP_URL=$(curl $DOWNLOAD_URL | jq -r ".versions[] | select(.version==\""$CHROME_VERSION"\").downloads.chromedriver[] | select(.platform==\""linux64"\") .url") \
    && wget --no-check-certificate --no-verbose -O /tmp/chrome_driver.zip $ZIP_URL \
    #  Unzip and create the /usr/local/bin/chromedriver executable  (-j means don't create a subfolder with the name of the archive; unzip in the folder directly)
    && unzip -j /tmp/chrome_driver.zip -d /usr/local/bin/chrome \
    && rm -f /tmp/chrome_driver.zip \
    && chmod +x /usr/local/bin/chrome/chrome \
    # Make some cleanup
    && apt-get clean \
    && rm -rf /tmp/* /var/lib/apt/lists/*

# Get latest composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

# Where we'll put our script
RUN mkdir /opt/behat

# Set working directory
WORKDIR /opt/behat
```

* Créez un fichier appelé `compose.yaml` avec ce contenu :

**compose.yaml**

```yaml
# cspell:disable
services:
  app:
    build:
      context: .
      args:
        # Please update IDs below if you've another values by running "id -u" (UID) and "id -g" (GID) in your Linux console
        - DOCKER_UID=1000
        - DOCKER_GID=1000
        # The name of the user in Docker; didn't have any importance
        - USERNAME=johndoe
    container_name: php-app
    working_dir: /opt/behat
    volumes:
      # We'll synchronize our local project folder (on our machine) with the Docker container
      - ./:/opt/behat
```

* Lancez `docker compose up --detach` pour créer votre container Docker

Nous allons vérifier que le driver Chrome est bien installé en lançant `docker compose exec -u $(id -u):$(id -g) app /usr/local/bin/chrome/chromedriver --version`. Idem pour le navigateur Chrome : `docker compose exec -u $(id -u):$(id -g) app /usr/local/bin/chrome/chrome --version`. Parfait ! Voir ces valeurs signifie que Chrome est prêt à être utilisé.

![Versions de Chrome](./images/chrome_versions.webp)

Ok, encore une chose : nous devons créer notre fichier `composer.json` puisqu'il faut inclure la dépendance PHP `Behat`.

* Lancez `docker compose exec -u $(id -u):$(id -g) app /bin/bash` pour démarrer un shell interactif dans votre container Docker en étant vous-même (c'est-à-dire qu'avec `-u $(id -u):$(id -g)`, les fichiers et dossiers créés dans Docker vous appartiendront),

* Lancez `composer init` pour démarrer l'assistant composer ; saisissez les valeurs que vous voulez, par exemple
  * Package name : `johndoe/behat`,
  * Description : `Introduction to Behat`,
  * Author : `John Doe`,
  * Minimum Stability : *appuyez simplement sur Entrée*,
  * Package Type : `project`,
  * License : *appuyez simplement sur Entrée*,
  * Composer vous demandera si vous voulez définir des dépendances et des dépendances de dev ; répondez `no` deux fois pour l'instant,
  * La question suivante concerne PSR-4, *appuyez simplement sur Entrée*,
  * Enfin, répondez `yes` au message de confirmation.

Maintenant, si vous êtes curieux, vous verrez que vous avez un nouveau fichier appelé `composer.json` et deux dossiers appelés `src` et `vendor`.

![Composer a été installé](./images/composer_installed.webp)

## Installons Behat {#lets-install-behat}

Comme indiqué dans la documentation [How to install?](https://docs.behat.org/en/latest/quick_start.html#installation), il suffit d'exécuter `composer require --dev behat/behat:^3` pour installer Behat comme dépendance de dev.

> **Information:**
>
> Assurez-vous d'être toujours dans une session Bash interactive dans le container Docker avant de lancer `composer require --dev behat/behat:^3`. Une session Bash interactive se démarre en lançant `docker compose exec -u $(id -u):$(id -g) app /bin/bash`.

![Installer Behat](./images/composer_install_behat.webp)

Une fois installé, vous pouvez démarrer Behat en lançant `vendor/bin/behat` mais, pour l'instant, vous obtiendrez une erreur et c'est parfaitement normal puisque nous devons d'abord écrire notre premier scénario.

![FeatureContext introuvable](./images/FeatureContext_not_found.webp)

Comme indiqué dans la documentation officielle, lancez simplement `vendor/bin/behat --init` pour créer les fichiers minimums requis.

![Exécution de behat init](./images/behat_init.webp)

Maintenant, si vous regardez votre dossier, vous voyez un nouveau dossier appelé `features/bootstrap` avec un fichier appelé `FeatureContext.php`. La capture ci-dessous illustre à quoi ressemble le projet dans mon éditeur VSCode à ce stade du tutoriel :

![FeatureContext dans vscode](./images/vscode_FeatureContext.webp)

## Écrire vos étapes en PHP {#writing-your-steps-in-php}

Cela doit être placé dans un fichier portant l'extension `.feature` dans le dossier `features` ; créons le fichier `Blog.feature` (montré au début de cet article) avec ce contenu :

![Le fichier Blog.feature](./images/blog_feature.webp)

> **Pensez à installer `Cucumber (Gerkin) Full Support`:**
>
> Si vous utilisez Visual Studio Code, vous pouvez installer [Cucumber (Gerkin) Full Support](https://marketplace.visualstudio.com/items?itemName=alexkrechik.cucumberautocomplete) pour obtenir la coloration et une meilleure intégration dans vscode, comme l'autocomplétion.

Maintenant que nous avons notre scénario, nous devons apprendre à Behat comment *traduire* ces phrases (c'est le [langage Gherkin](https://cucumber.io/docs/gherkin/)) dans notre code PHP.

Ouvrez maintenant le fichier `features/bootstrap/FeatureContext.php` dans votre éditeur préféré :

![Édition du fichier FeatureContext.php](./images/vscode_edit_0_FeatureContext.webp)

Comme vous le voyez, c'est le fichier par défaut créé plus tôt. Nous devons y écrire du code PHP, mais comment ?

Retournez dans votre console Bash interactive et lancez `vendor/bin/behat --dry-run --append-snippets`. Cela demande à Behat de créer les étapes pour vous, automagiquement.

![Append snippets](./images/append_snippets.webp)

Tapez `1` pour demander à Behat d'utiliser votre fichier `FeatureContext.php`. Vous obtiendrez alors une confirmation à l'écran :

```bash
u features/bootstrap/FeatureContext.php - `I am on "https://www.avonture.be"` definition added
u features/bootstrap/FeatureContext.php - `I click on the "Blog" menu item` definition added
u features/bootstrap/FeatureContext.php - `I should be on "/blog"` definition added
```

Ouvrez à nouveau le fichier `features/bootstrap/FeatureContext.php` dans votre éditeur préféré :

![Nous avons maintenant nos étapes](./images/vscode_edit_1_FeatureContext.webp)

Sympa, non ? Behat a regardé notre fichier `Blog.feature` et a créé autant de méthodes dans notre code PHP que nous avions de phrases dans notre scénario, et chaque méthode est associée à sa phrase (regardez le commentaire PHP avant chaque méthode).

Et maintenant, avant même de commencer à coder, demandons à Behat d'exécuter notre scénario ; lancez `vendor/bin/behat` dans la console — c'est la sortie « Premier lancement » montrée au début de cet article.

Wow ! Jusqu'ici, parfait ! Nous avons donc demandé à Behat d'exécuter notre scénario et il sait que nous avons trois étapes et qu'il nous reste à écrire le code associé en PHP (d'où le message **TODO: write pending definition** en jaune).

Donnons-nous les moyens de faire au mieux le plus vite possible, sans réinventer la roue si quelqu'un d'autre l'a déjà fait. Installons deux nouvelles dépendances. Dans votre console, lancez `composer require --dev friends-of-behat/mink` et `composer require --dev dmore/behat-chrome-extension`.

`friends-of-behat/mink` est une très chouette dépendance qui arrive avec des étapes déjà prédéfinies : faire un login, naviguer vers des pages, vérifier que la page courante est ... Et en utilisant `friends-of-behat/mink`, la vie sera plus simple et nous n'aurons pas à réinventer la roue.

## Utiliser Mink {#using-mink}

Maintenant que Mink est installé, allez dans votre éditeur, ouvrez le fichier `features/bootstrap/FeatureContext.php` et remplacez la ligne `class FeatureContext implements Context` par `class FeatureContext extends \Behat\MinkExtension\Context\MinkContext`.

![Utilisation de MinkContext](./images/MinkContext.webp)

Ce petit changement nous donne beaucoup de pouvoir car notre code utilise désormais Mink et bénéficie donc de toutes ses méthodes existantes.

Pour obtenir la liste des méthodes existantes, lançons `clear ; vendor/bin/behat -di` dans votre console :

![Affichage des définitions](./images/print_definitions.webp)

Vous avez vu ? Nous avons nos trois étapes (les trois premières affichées) puis des étapes supplémentaires venant de Mink. Et vous pouvez scroller longtemps : il y a déjà beaucoup d'étapes que Mink vous permet de réutiliser.

Relancez `vendor/bin/behat` dans la console :

![Correspondance ambiguë](./images/ambiguous_match.webp)

Nous obtenons l'erreur *Ambiguous match of ...* sur la toute première étape, notre étape *I am on <https://www.avonture.be>*. Ambiguë signifie que nous essayons d'ajouter du code PHP dans notre contexte alors que cette étape est déjà définie (par Mink). Donc, supprimez-la simplement : éditez votre fichier `FeatureContext.php`, sélectionnez `public function iAmOn` et supprimez toute la fonction.

![Supprimer la méthode iAmOn](./images/drop_iamon.webp)

En relançant `vendor/bin/behat`, vous aurez la même erreur pour `iShouldBeOn` ; supprimez donc aussi cette fonction.

Lancez `clear ; vendor/bin/behat` une fois de plus et nous obtenons maintenant une nouvelle erreur :

![Instance Mink non définie](./images/mink_instance_not_set.webp)

## Installer l'extension Mink {#installing-the-mink-extension}

Nous devons maintenant référencer `behat-chrome-extension` dans un fichier appelé `behat.yml`.

Créez le fichier `behat.yml` à la racine de votre projet, avec ce contenu :

**behat.yaml**

```yaml
default:
    extensions:
        DMore\ChromeExtension\Behat\ServiceContainer\ChromeExtension: ~
        Behat\MinkExtension:
            browser_name: chrome
            base_url: https://www.avonture.be
            sessions:
                default:
                    chrome:
                      api_url: http://0.0.0.0:9222
```

Éditez également le fichier `features/bootstrap/FeatureContext.php`, supprimez tout et remplacez par le contenu ci-dessous.

Nous avons modifié quelques instructions `use` pour ajouter les bibliothèques Mink (et retirer celles qui ne servent plus). Nous avons aussi ajouté une propriété privée `$mink` et quelques lignes dans le `__constructor`.

> **Pensez à adapter l'url `https://www.avonture.be` à votre site:**

**features/bootstrap/FeatureContext.php**

```php
<?php

use Behat\Mink\Mink;
use Behat\Mink\Session;
use DMore\ChromeDriver\ChromeDriver;
use Behat\Behat\Tester\Exception\PendingException;

/**
 * Defines application features from the specific context.
 */
class FeatureContext extends \Behat\MinkExtension\Context\MinkContext
{

    private Behat\Mink\Mink $mink;

    /**
     * Initializes context.
     *
     * Every scenario gets its own context instance.
     * You can also pass arbitrary arguments to the
     * context constructor through behat.yml.
     */
    public function __construct()
    {
        $this->mink = new Mink(
            [
                'browser' => new Session(
                    new ChromeDriver(
                        'http://0.0.0.0:9222',
                        null,
                        "https://www.avonture.be"  // <-- Think to update to the site you wish to test
                    )
                )
            ]
        );

    }

    /**
     * @Then I click on the :arg1 menu item
     */
    public function iClickOnTheMenuItem($arg1)
    {
        throw new PendingException();
    }
}
```

En relançant `vendor/bin/behat`, nous avons maintenant une autre erreur :

![Could not fetch version information](./images/could_not_fetch.webp)

## Place à Chrome {#time-to-run-chrome}

Créez un fichier appelé `run.sh` avec ce contenu :

**run.sh**

```bash
#!/usr/bin/env bash

clear

# We'll start Chrome on URL http://0.0.0.0:9222
# If this URL has to be modified, think to update the constructor of FeatureContext.php too
# Note: /usr/bin/google-chrome-stable has been installed in our Dockerfile
nohup /usr/local/bin/chrome/chrome --headless --remote-debugging-address=0.0.0.0 \
    --remote-debugging-port=9222 --no-sandbox --window-size="1920,1080" --disable-dev-shm-usage \
    --disable-extensions --no-startup-window --no-first-run --no-pings > /dev/null 2>&1 &

# Get the process ID of chrome so we can kill the process when we've finished
chromePID=$!

# shellcheck disable=SC2048,SC2086
php -d memory_limit=-1 "vendor/bin/behat" --config=behat.yaml

if ((chromePID > 0)); then
    kill "${chromePID}" > /dev/null 2>&1
fi
```

Puis rendez le fichier exécutable en lançant `chmod +x ./run.sh`.

Vous pouvez maintenant démarrer Behat en lançant `./run.sh` dans votre console.

Si tout se passe bien, vous obtiendrez ceci :

![Premier lancement](./images/run_sh_pending.webp)

Comme vous le voyez, la ligne `Given I am on...` est en vert : Behat, grâce à notre driver Chrome, a réussi à atteindre la page.

Ensuite, la ligne `Then I click on the "Blog" menu item` est en jaune et c'est normal puisque nous n'avons pas encore créé la fonction et, logiquement, `Then I should be on "/blog"` est en bleu parce que cette phrase n'a pas encore été exécutée (skippée).

## Écrire notre méthode iClickOnTheMenuItem {#writing-our-iclickonthemenuitem-method}

Retour au fichier `bootstrap/FeatureContext.php`. Remplacez la méthode `iClickOnTheMenuItem` par ce code :

**features/bootstrap/FeatureContext.php**

```php
/**
 * @Then I click on the :menuItem menu item
 */
public function iClickOnTheMenuItem(string $menuItem): void
{
    if (!is_dir('.output')) {
        mkdir('.output', 0777);
    }

    // When entering in the method, take a screenshot so we can assert we're on the homepage
    file_put_contents('.output/homepage.png', $this->getSession()->getDriver()->getScreenshot());

    throw new PendingException();
}
```

Comme vous le voyez, nous allons créer un dossier `.output` et la seule chose que nous faisons est de prendre une capture d'écran.

Juste après avoir relancé `./run.sh` dans votre console, vous devriez voir un nouveau dossier `.output` avec une image :

![Nous sommes sur la page d'accueil](./images/screenshot_homepage.webp)

Qu'avons-nous fait ? Nous avons vérifié que lorsque Behat exécute notre méthode `iClickOnTheMenuItem`, Chrome accède bien à notre page d'accueil. Nous pouvons maintenant simuler un clic sur l'entrée de menu voulue : le menu Blog stocké dans notre paramètre $menuItem.

Remplacez la méthode par ce nouveau code :

**features/bootstrap/FeatureContext.php**

```php
/**
 * @Then I click on the :menu menu item
 */
public function iClickOnTheMenuItem(string $menu): void
{
    if (!is_dir('.output')) {
        mkdir('.output', 0777);
    }

    // When entering in the method, take a screenshot so we can assert we're on the homepage
    file_put_contents('.output/homepage.png', $this->getSession()->getDriver()->getScreenshot());

    /**
     * @var $elements Behat\Mink\Element\NodeElement
     */
    $elements = $this->getSession()->getPage()->findAll('css', 'a[class="navbar__item navbar__link"]');

    // Now we'll get all links from our navigation bar. $elements will contain more than one link in our case.
    // The CSS selector has been written using our Web developer console; in Edge or Chromium for instance.
    // Loop any elements (our navigation links) and when we've found the one having `Blog` as text, click on it
    foreach ($elements as $element) {
        if ($element->getText() === $menu) {
            $element->click();
        }
    }

    // Give one second to the website to handle the click and, so, to navigate to, as we expect it, the `/blog` path
    sleep(1);

    // Take a second image, now we should be in the list of posts
    file_put_contents('.output/iClickOnBlog.png', $this->getSession()->getDriver()->getScreenshot());
}
```

Lancez `./run.sh` une fois de plus et bingo !

![Succès](./images/success.webp)

C'est un succès parce que le `Then I should be on "/Blog"` a été vérifié par Mink. Nous n'avons pas besoin d'écrire la méthode `Then I should be on`, elle est déjà définie par Mink.

Essayons autre chose : retour dans le PHP et remplacez `if ($element->getText() === $menu) {` par `if ($element->getText() === 'Archive') {`. Nous ne cliquerons donc plus sur le menu Blog mais sur `Archive` ; voyons ce qui se passe :

![Échec](./images/failure.webp)

Et ça échoue ; parfait !

## Conclusion {#conclusion}

C'est la fin de ce tutoriel.

Avec Behat et Mink, nous pouvons automatiser les tests fonctionnels.

Nous allons pouvoir écrire des scénarios, simuler des actions utilisateur comme cliquer sur n'importe quel objet de la page, uploader des documents, faire des choses comme insérer un nouvel article de blog puis injecter du contenu dans l'éditeur et, après avoir enregistré l'élément, visiter la page du blog, la trier par date et vérifier que notre nouvel article s'affiche bien et qu'il est le premier de la liste.

Nous pouvons le faire avant que l'application existe (concept du BDD), pendant le codage (pour s'assurer que les fonctionnalités marchent toujours, même après un gros refactoring par exemple) ou pendant tout le cycle de vie (pour s'assurer que l'application fonctionne toujours comme prévu).

Continuez votre exploration grâce à ces sites :

* [Automate testing with Behat](https://docs.pantheon.io/behat),
* [Behat-chrome-extension repository](https://gitlab.com/behat-chrome/behat-chrome-extension),
* [Behat: The Easy and Effective Way to Write Acceptance Tests](https://dev.to/jszutkowski/behat-the-easy-and-effective-way-to-write-acceptance-tests-cm4),
* [Behat - Official website](https://docs.behat.org/en/latest/),
* [Cucumber - Gherkin Tutorial](https://cucumber.io/),
* [DMore - Chrome Mink Driver](https://packagist.org/packages/dmore/chrome-mink-driver) et
* [Mink at a Glance](https://mink.behat.org/en/latest/at-a-glance.html)

## Annexe - Liste des fichiers, leur contenu et comment les exécuter {#appendix---list-of-files-and-their-contents-and-how-to-run-them}

Dans ce tutoriel, nous avons manipulé beaucoup de fichiers et de commandes. Pour vous permettre de vérifier que votre version est bien celle utilisée à la fin de cet article, vous trouverez ci-dessous chaque fichier avec son contenu en fin de tutoriel.

Pour rappel, quand tous les fichiers ci-dessous ont été créés sur votre disque, vous devez d'abord créer votre image Docker et un container ; cela se fait en lançant `docker compose up --detach` dans votre console.

La deuxième commande à lancer est `docker compose exec -u $(id -u):$(id -g) app composer install` pour installer toutes les dépendances PHP.

Enfin, pour exécuter notre script `run.sh`, vous devez lancer `docker compose exec -u $(id -u):$(id -g) app ./run.sh`

### behat.yaml {#behatyaml}

**behat.yaml**

```yaml
default:
    extensions:
        DMore\ChromeExtension\Behat\ServiceContainer\ChromeExtension: ~
        Behat\MinkExtension:
            browser_name: chrome
            base_url: https://www.avonture.be
            sessions:
                default:
                    chrome:
                      api_url: http://0.0.0.0:9222
```

### Blog.feature {#blogfeature}

Le nom de fichier relatif est `features/Blog.feature`.

**features/Blog.feature**

```feature
Feature: Clicking on the Blog menu item should gives me the list of articles

  Scenario: I click on the Blog menu
    Given I am on "https://www.avonture.be"
    Then I click on the "Blog" menu item
    Then I should be on "/blog"
```

### composer.json {#composerjson}

**composer.json**

```json
{
    "name": "johndoe/behat",
    "description": "Introduction to behat",
    "type": "project",
    "autoload": {
        "psr-4": {
            "Johndoe\\Behat\\": "src/"
        }
    },
    "authors": [
        {
            "name": "John Doe"
        }
    ],
    "require-dev": {
        "behat/behat": "^3",
        "behat/mink-extension": "^2.3",
        "dmore/behat-chrome-extension": "^1.4",
        "friends-of-behat/mink": "^1.11"
    }
}
```

### compose.yaml {#composeyaml}

**compose.yaml**

```yaml
# cspell:disable
services:
  app:
    build:
      context: .
      args:
        # Please update IDs below if you've another values by running "id -u" (UID) and "id -g" (GID) in your Linux console
        - DOCKER_UID=1000
        - DOCKER_GID=1000
        # The name of the user in Docker; didn't have any importance
        - USERNAME=johndoe
    container_name: php-app
    working_dir: /opt/behat
    volumes:
      # We'll synchronize our local project folder (on our machine) with the Docker container
      - ./:/opt/behat
```

### Dockerfile {#dockerfile}

**Dockerfile**

```docker
# cspell:disable

# The user ID and name and group ID of the user to create in our image
ARG DOCKER_UID=1000
ARG DOCKER_GID=1000
ARG USERNAME=johndoe

# The version of Chrome we'll install
ARG CHROME_VERSION=115.0.5763.0

# This is a PHP project
FROM php:8.2-fpm

# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends git jq zip unzip wget gnupg \
    && apt-get clean \
    && rm -rf /tmp/* /var/lib/apt/lists/*

# We'll create a new user with the same uid/gid than ours, on our host machine.
ARG DOCKER_UID
ARG DOCKER_GID
ARG USERNAME

RUN groupadd --gid ${DOCKER_GID} "${USERNAME}" \
    && useradd --home-dir /home/"${USERNAME}" --create-home --uid ${DOCKER_UID} \
        --gid ${DOCKER_GID} --shell /bin/sh --skel /dev/null "${USERNAME}"

# Download and install Chrome
ARG CHROME_VERSION
ARG DOWNLOAD_URL="https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json"

# Download Chrome (the browser and the driver)
RUN wget --no-check-certificate --no-verbose -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
    && sh -c 'echo "deb https://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
    && apt-get update -yqq \
    && apt-get install -y --no-install-recommends -yq gconf-service libasound2 libatk1.0-0 libc6 libcairo2 \
        libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libgconf-2-4 \
        libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 \
        libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 \
        libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 \
        libxss1 libxtst6 ca-certificates fonts-liberation libnss3 lsb-release \
        xdg-utils \
    && apt-get clean \
    && rm -rf /tmp/* /var/lib/apt/lists/* \
    # --- Download the browser ---
    && ZIP_URL=$(curl $DOWNLOAD_URL | jq -r ".versions[] | select(.version==\""$CHROME_VERSION"\").downloads.chrome[] | select(.platform==\""linux64"\") .url") \
    && wget --no-check-certificate --no-verbose -O /tmp/chrome_browser.zip $ZIP_URL \
    && printf "\e[0;105m%s\e[0;0m\n" "Using chromedriver $(/usr/local/bin/chrome/chromedriver --version)" \
    #  Unzip and create the /usr/local/bin/chromedriver executable  (-j means don't create a subfolder with the name of the archive; unzip in the folder directly)
    && unzip -j /tmp/chrome_browser.zip -d /usr/local/bin/chrome \
    && rm -f /tmp/chrome_browser.zip \
    && ls -alh /usr/local/bin/chrome \
    && chmod +x /usr/local/bin/chrome/chrome \
    # --- Download the driver ---
    && ZIP_URL=$(curl $DOWNLOAD_URL | jq -r ".versions[] | select(.version==\""$CHROME_VERSION"\").downloads.chromedriver[] | select(.platform==\""linux64"\") .url") \
    && wget --no-check-certificate --no-verbose -O /tmp/chrome_driver.zip $ZIP_URL \
    #  Unzip and create the /usr/local/bin/chromedriver executable  (-j means don't create a subfolder with the name of the archive; unzip in the folder directly)
    && unzip -j /tmp/chrome_driver.zip -d /usr/local/bin/chrome \
    && rm -f /tmp/chrome_driver.zip \
    && chmod +x /usr/local/bin/chrome/chrome \
    # Make some cleanup
    && apt-get clean \
    && rm -rf /tmp/* /var/lib/apt/lists/*

# Get latest composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

# Where we'll put our script
RUN mkdir /opt/behat

# Set working directory
WORKDIR /opt/behat
```

### FeatureContext.php {#featurecontextphp}

Le nom de fichier relatif est `features/bootstrap/FeatureContext.php`.

**features/bootstrap/FeatureContext.php**

```php
<?php

use Behat\Mink\Mink;
use Behat\Mink\Session;
use DMore\ChromeDriver\ChromeDriver;
use Behat\Behat\Tester\Exception\PendingException;

/**
 * Defines application features from the specific context.
 */
class FeatureContext extends \Behat\MinkExtension\Context\MinkContext
{

    private Behat\Mink\Mink $mink;

    /**
     * Initializes context.
     *
     * Every scenario gets its own context instance.
     * You can also pass arbitrary arguments to the
     * context constructor through behat.yml.
     */
    public function __construct()
    {
        $this->mink = new Mink(
            [
                'browser' => new Session(
                    new ChromeDriver(
                        'http://0.0.0.0:9222',
                        null,
                        "https://www.avonture.be"  // <-- Think to update to the site you wish to test
                    )
                )
            ]
        );
    }

    /**
     * @Then I click on the :menu menu item
     */
    public function iClickOnTheMenuItem(string $menu): void
    {
        if (!is_dir('.output')) {
            mkdir('.output', 0777);
        }

        // When entering in the method, take a screenshot so we can assert we're on the homepage
        file_put_contents('.output/homepage.png', $this->getSession()->getDriver()->getScreenshot());

        /**
         * @var $elements Behat\Mink\Element\NodeElement
         */
        $elements = $this->getSession()->getPage()->findAll('css', 'a[class="navbar__item navbar__link"]');

        // Now we'll get all links from our navigation bar. $elements will contain more than one link in our case.
        // The CSS selector has been written using our Web developer console; in Edge or Chromium for instance.
        // Loop any elements (our navigation links) and when we've found the one having `Blog` as text, click on it
        foreach ($elements as $element) {
            if ($element->getText() === $menu) {
                $element->click();
            }
        }

        // Give one second to the website to handle the click and, so, to navigate to, as we expect it, the `/blog` path
        sleep(1);

        // Take a second image, now we should be in the list of posts
        file_put_contents('.output/iClickOnBlog.png', $this->getSession()->getDriver()->getScreenshot());
    }
}
```

### run.sh {#runsh}

**run.sh**

```bash
#!/usr/bin/env bash

clear

# We'll start Chrome on URL http://0.0.0.0:9222
# If this URL has to be modified, think to update the constructor of FeatureContext.php too
# Note: /usr/bin/google-chrome-stable has been installed in our Dockerfile
nohup /usr/local/bin/chrome/chrome --headless --remote-debugging-address=0.0.0.0 \
    --remote-debugging-port=9222 --no-sandbox --window-size="1920,1080" --disable-dev-shm-usage \
    --disable-extensions --no-startup-window --no-first-run --no-pings > /dev/null 2>&1 &

# Get the process ID of chrome so we can kill the process when we've finished
chromePID=$!

# shellcheck disable=SC2048,SC2086
php -d memory_limit=-1 "vendor/bin/behat" --config=behat.yaml

if ((chromePID > 0)); then
    kill "${chromePID}" > /dev/null 2>&1
fi
```
