tests Behat/Mink sous docker en Symfony3 & 4

Récemment, j’ai dû mettre en place des tests BEHAT/MINK sous docker. Je l’avais fait sur mon poste en local en faisant tourner un jar selenium et m’étais dit: « oh, ça ne va pas être bien compliqué ». Que nenni! J’ai littéralement lutté pour faire tourner le tout (sachant qu’en plus, je devais le faire tourner sur un gitlab-ci après).

J’aimerais vous épargner de longues heures de recherche. La plupart des tutoriaux sur lesquels je suis tombé étaient soit obsolètes (Symfony2), soit ne fonctionnaient pas, soit faisaient appel à des outils n’étant plus maintenus.

J’ai tenté phantomJS pour avoir un browser headless mais il n’est plus maintenu et ça n’a pas fonctionné à cause du GhostDriver.

J’ai aussi tenté l’option selenium + chromedriver + chrome en version headless mais les perfs n’étaient pas terribles.

J’ai réussi à trouver deux solutions:

  • une avec selenium et chrome en mode headless (à partir de la version 59)
  • une sans selenium en connexion directe avec chrome en mode headless

Autant vous dire que les perfs ne sont pas les mêmes. Une même page peut mettre 1,6s à s’afficher avec selenium contre 0.5s uniquement avec chrome.

Commençons avec le fichier composer.json

"require-dev": {
        "behat/behat": "^3.5",
        "behat/mink": "^1.7",
        "behat/mink-browserkit-driver": "^1.3",
        "behat/mink-extension": "^2.3",
        "behat/mink-selenium2-driver": "^1.3",
        "behat/symfony2-extension": "^2.1",
        "behatch/contexts": "^3.2"
    },

behatch/contexts permet de rajouter des steps supplémentaires à ceux existants.

 

D’abord, un test avec le tag @javascript

En fait, ce test ici n’a que peu de sens car je ne teste pas de javascript. Mais pour tester la solution en mode browser headless, je dois rajouter ce tag:

Feature:
  I want to visit homepage

Background:
  When I am on homepage

  @javascript
  Scenario:
    Then I should see "UNTITLED vous propose une collection régulièrement actualisée d'illustrations et posters des meilleurs d'artistes du monde de la création numérique."
    Then I should see "Voir la galerie"
    Then I should see "Actualités"

 

Sans plus attendre, voici les deux solutions que j’ai trouvées:

AVEC SELENIUM

Le docker-compose

version: '3'

services:
    php:
        container_name: php_galerie
        build: docker/php
        volumes:
            - ./:/var/www/project
        depends_on:
            - chrome
        environment:
            VIRTUAL_HOST: galerie-local.fr
        working_dir: /var/www/project
        networks:
            - app

    hub:
        image: selenium/hub
        ports:
            - 4444:4444
        networks:
            - app

    chrome:
        image: selenium/node-chrome
        restart: always
        links:
            - hub
        environment:
            - HUB_HOST=hub
            - HUB_PORT=4444
        external_links:
            - reverseproxy:galerie-local.fr
        networks:
            - app

    reverseproxy:
        image: jwilder/nginx-proxy
        ports:
            - 80:80
            - 443:443
        volumes:
            - /var/run/docker.sock:/tmp/docker.sock
        networks:
            - app

networks:
    app:
        driver: bridge

Je n’ai pas mis le container de base de données dedans pour simplifier le docker-compose.

Plusieurs points à noter:

  • depends_on du container php par rapport au container chrome
  • le VIRTUAL_HOST, très important, car il va servir au reverse proxy pour enregistrer ce host
  • la propriété external_links pour permettre au container de l’image chrome de « demander » le host au reverse proxy nginx (sinon behat ignore l’host à appeler pour chaque uri)

Je vous avoue que j’ai trouvé ça tordu de devoir faire intervenir un reverse proxy pour ça mais si j’avais lié l’image chrome à l’image php (et que j’avais dit que l’url était alors php et pas galerie-local.fr), j’aurais eu un problème de référence circulaire puisque l’image php est déjà liée à l’image chrome par le « depends_on ». Pas simple, n’est-ce pas?

 

La config behat.yml en Symfony3

default:
    autoload:
        '': '%paths.base%/../../tests'
    extensions:
        Behat\MinkExtension:
            base_url: 'http://galerie-local.fr'
            browser_name: chrome
            javascript_session: chrome_javascript_session
            sessions:
              default:
                symfony2: ~
              chrome_javascript_session:
                selenium2:
                  wd_host: 'http://172.17.0.1:4444/wd/hub'
        Behat\Symfony2Extension: ~
        Behatch\Extension: ~
    suites:
        web:
          mink_session: default
          type: symfony_bundle
          bundle: AppBundle
          paths:
            - '%paths.base%/../../tests/AppBundle/Features/Test'
          contexts:
            - Behatch\Context\BrowserContext
            - AppBundle\Context\TransactionContext

 

La config behat.yml en Symfony4

default:
    autoload: '%paths.base%/features/bootstrap'
    extensions:
        Behatch\Extension: ~
        Behat\Symfony2Extension:
            kernel:
                bootstrap: features/bootstrap/bootstrap.php
                class: App\Kernel
        Behat\MinkExtension:
            browser_name: chrome
            base_url: "http://galerie-local.fr"
            javascript_session: chrome_selenium_session
            sessions:
                default:
                    symfony2: ~
                chrome_selenium_session:
                    selenium2:
                        wd_host: "http://172.17.0.1:4444/wd/hub"
    suites:
        interface:
            mink_session: default
            paths:
                - '%paths.base%/features'
            contexts:
                - App\Tests\Behat\Context\CoreContext: []
                - Behatch\Context\BrowserContext: []

 

Il n’y a plus qu’à faire un docker-compose up -d, puis à lancer les tests behat:

docker exec -it php_galerie bash -c 'vendor/bin/behat -vvv'

SANS SELENIUM AVEC CHROME HEADLESS

La config docker-compose

Là, du coup, la config reste la même que celle que vous aviez: pas de container chrome ou hub. La seule chose est qu’il faut avoir pour le container php un extra_hosts et monter un volume supplémentaire (/dev/shm):

    php:
        container_name: php_galerie
        build: docker/php
        volumes:
            - ./:/var/www/project
            - /dev/shm:/dev/shm
        extra_hosts:
            - 'localhost galerie-local.fr:127.0.0.1'
        ports:
            - 80:80
        working_dir: /var/www/project

Sans le montage du volume /dev/shm vers /dev/shm, chrome essaiera d’écrire dans /tmp et ce dernier étant limité à 63M dans un container, il vous dira que  vous devez en avoir au moins 64 pour qu’il puisse fonctionner.

De même, l’extra_hosts est important sinon il sera impossible d’atteindre le site à partir du container (tentez de faire un curl http://votresite.local dans le container php_galerie et vous aurez une erreur).

Dans votre Dockerfile, vous devez installer chrome:

# if you are on debian jessie, uncomment this line
#RUN printf "deb http://archive.debian.org/debian/ jessie main\ndeb-src http://archive.debian.org/debian/ jessie main\ndeb http://security.debian.org jessie/updates main\ndeb-src http://security.debian.org jessie/updates main" > /etc/apt/sources.list
RUN set -xe \
    && apt-get update \
    && apt-get install -y --no-install-recommends curl \
    && rm -rf /var/lib/apt/lists/*

RUN set -xe \
    && curl -fsSL https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
    && echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list \
    && apt-get update \
    && apt-get install -y google-chrome-stable \
    && rm -rf /var/lib/apt/lists/*

 

Le script sh de lancement dans docker

Ensuite, dans votre script docker de lancement (mon fichier s’appelle run.sh et se lance au build du container php), vous devez mettre:

google-chrome-stable --disable-gpu --headless --window-size=1920,1080 --no-sandbox --remote-debugging-address=0.0.0.0 --remote-debugging-port=9222 &

Mise à jour du composer.json

Vous devez rajouter ce vendor: « dmore/behat-chrome-extension »: « ^1.3 »

Mise à jour du fichier behat.yml

default:
    autoload: '%paths.base%/features/bootstrap'
    extensions:
        DMore\ChromeExtension\Behat\ServiceContainer\ChromeExtension: ~
        Behatch\Extension: ~
        Behat\Symfony2Extension:
            kernel:
                bootstrap: features/bootstrap/bootstrap.php
                class: App\Kernel
        Behat\MinkExtension:
            browser_name: chrome
            base_url: "http://galerie-local.fr"
            default_session: default
            javascript_session: chrome_selenium_session
            sessions:
                default:
                    symfony2: ~
                chrome_selenium_session:
                    selenium2:
                        wd_host: "http://172.17.0.1:4444/wd/hub"
    suites:
        interface:
            mink_session: default
            paths:
                - '%paths.base%/features'
            contexts:
                - App\Tests\Behat\Context\CoreContext: []


chrome_headless_local_session:
    extensions:
        Behat\MinkExtension:
            javascript_session: chrome_headless_session
            sessions:
                chrome_headless_session:
                    chrome:
                        api_url: 'http://php:9222'

Je l’ai mis en Symfony4, mais les ajouts sont facilement transposables en Symfony3.

Les changements sont:

  • Rajout de l’extension: DMore\ChromeExtension\Behat\ServiceContainer\ChromeExtension: ~
  • Rajout d’un nouveau profil. Si vous vous demandez pourquoi j’ai mis php à la place d’une ip, c’est parce que j’ai besoin de l’ip dynamique du container php_galerie pour que chrome puisse s’exécuter (en local sans docker, ce serait 127.0.0.1)

J’ai laissé la configuration selenium pour vous montrer que l’on peut combiner les deux, mais si vous désirez la version minimaliste, ce serait:

default:
    autoload: '%paths.base%/features/bootstrap'
    extensions:
        DMore\ChromeExtension\Behat\ServiceContainer\ChromeExtension: ~
        Behatch\Extension: ~
        Behat\Symfony2Extension:
            kernel:
                bootstrap: features/bootstrap/bootstrap.php
                class: App\Kernel
        Behat\MinkExtension:
            browser_name: chrome
            base_url: "http://galerie-local.fr"
            default_session: default
            javascript_session: chrome_headless_session
            sessions:
                default:
                    symfony2: ~
                chrome_headless_session:
                    chrome:
                        api_url: 'http://php:9222'
    suites:
        interface:
            mink_session: default
            paths:
                - '%paths.base%/features'
            contexts:
                - App\Tests\Behat\Context\CoreContext: []

 

Il n’y a plus qu’à faire un docker-compose up -d, puis à lancer les tests behat:

docker exec -it php_galerie bash -c 'vendor/bin/behat -p chrome_headless_local_session -vvv'

ou avec la config allégée:

docker exec -it php_galerie bash -c 'vendor/bin/behat -vvv'

Conclusion

J’espère vous avoir épargné de longues heures de recherche. J’ai trouvé vraiment très peu de doc sur le net et elles étaient souvent pour Symfony2. La solution avec selenium fonctionnait bien mais était un peu lente à mon goût et je voulais que mes tests s’exécutent le plus rapidement possible. La solution avec chrome headless me paraît idéale.

Rédigé par

One comment

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.