Tests fonctionnels en Drupal 8 avec Behat

  • #Architecture d'entreprise & applicative
  • #Frameworks & développements
  • #Communication/marketing/performances commerciales
  • #Gestion de sites & contenus

Publié le Mis à jour le Par

Behat, qu’est ce que c’est ?

Behat est un framework utilisé pour du “Behavior Driven Development” (BDD). Il permet de tester des scénarios rédigés dans un langage simple et naturel pour l’homme, qu’il transforme ensuite en tests applicatifs.

Pour exécuter les tests ainsi rédigés, Behat exécute un autre programme chargé de simuler le comportement d’un utilisateur : un émulateur. Il en existe un grand nombre : Goutte, Selenium, etc. et chacun a ses spécificités. Voici les caractéristiques des plus utilisées afin de vous aider à choisir :

 Selenium2 ZombieGoutte
Supporte le JSOuiOuiNon
Redimensionnement des fenêtresOuiNonNon
Manipulation des cookiesOuiOuiOui
Gestion des iframesOuiNonNon
Gestion des formulairesOuiOuiOui
Accès aux entêtes des requêtesNonOuiOui

À première vue, on peut voir que Selenium2 permet de tout faire mais il a aussi des limites : voici la documentation permettant d’avoir une vision plus complète : http://mink.behat.org/en/latest/guides/drivers.html

Installation

Nous avons choisi d’installer Behat en utilisant composer, il suffit de lancer la commande :

composer require behat/behat

À ce moment, composer installera toutes les dépendances nécessaires à Behat (voir la documentation officielle). Sur Drupal 8, nous avons préféré installer Behat à côté du dossier « web », ce qui permet de le distinguer des dossiers Drupal.

Une fois que composer a fini de s’exécuter, il faut alors procéder à l’initialisation de Behat en exécutant behat --init depuis le répertoire où Behat est installé.

À ce moment on voit l’apparition de deux fichiers très importants :

  • behat.yml : permettant de configurer Behat en lui ajoutant toutes les extensions souhaitées
  • FeatureContext.php : permettant d’ajouter des tests personnalisés

Configuration du behat.yml :

Il devrait se décomposer de la manière suivante :

  • Les contextes : Permet de rajouter des classes et paramètres nécessaires au projet
  • Les formatters : L’affichage du résultat
  • Les extensions : Ajout d’extensions comme celle de Drupal, ou celle permettant de faire des captures d’écran…
default:
  gherkin:
    filters:
      tags: ~@wip #rajouter @wip aux sénarios en cours de développement, ils seront ignorés. Pour les executer volontairement, lancer la commande `behat --tags=wip`
  suites:
    default:
      contexts:
        - FeatureContext
        - DrupalDrupalExtensionContextDrupalContext
        - DrupalDrupalExtensionContextMinkContext
        - DrupalDrupalExtensionContextMessageContext
        - DrupalDrupalExtensionContextDrushContext
  formatters:
     progress: #Ajout de progress pour ne pas voir le détail des étapes de chaque scénario sur le terminal. Pointillés à la place
       output_path: null
      pretty:  #Ajout de pretty pour voir le détail des étapes de chaque scénario sur le terminal
        output_path: null
      html:
        output_path: %paths.base%/build/html/behat
  extensions:
    BexBehatScreenshotExtension:
      image_drivers:
        local:
          screenshot_directory: "LIEN/DU/DOSSIER/POUR/LES/SCREENSHOTS"
          clear_screenshot_directory: true  # Enable removing all images before each test run. It is false by default.
    BehatMinkExtension:
      base_url:  URLDELAVM
      goutte: ~
      default_session: selenium2
      javascript_session: selenium2
      browser_name: 'chrome'
      selenium2:
        wd_host: "http://localhost:8643/wd/hub"
    DrupalDrupalEx tension:
      blackbox: ~
      api_driver: 'drupal'
      drupal:
        drupal_root: 'DOSSIER_CONTENANT_LE_DRUPAL'
      api_driver: 'drush'
      drush:
        root: 'DOSSIER_OU_DRUSH_EST_INSTALLE'
    emuseBehatHTMLFormatterBehatHTMLFormatterExtension:
          name: html
          renderer: Twig,Behat2
          file_name: index
          print_args: true
          print_outp: true
          loop_break: true 

Il faut aussi ajouter quelques modifications pour avoir un développement plus simple (pour d’autres ajouts, voici un lien qui devrait vous aider http://docs.behat.org/en/v2.5/guides/7.config.html) :

  • Ajout du tag @wip permettant d’ignorer tous les tests ayant ce tag lors de l’exécution des tests
  • Ajout des screenshots à chaque test non réussi
  • Choix de la forme d’affichage des tests (détaillée ou non) dans la section formatters
  • Ajout de PhantomJS supportant mieux le JS que selenium 2 (il faut cependant lancer le serveur PhantomJS après l’avoir installé sur la VM) voici les commandes pour le faire :
  cd /opt
  wget 
https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2
  tar -xjvf  phantomjs-2.1.1-linux-x86_64.tar.bz2
  mv phantomjs-2.1.1-linux-x86_64 phantomjs
  ln -s /opt/phantomjs/bin/phantomjs /usr/bin/phantomjs
  which phantomjs
  phantomjs --webdriver=8643 

La partie configuration s’arrête là pour le moment. Passons au coeur du sujet : les tests !

Tests

Les tests se créent selon l’arborescence suivante (si vous devez faire comme moi : création d’un dossier Behat à côté du dossier web) : behat/features.
Dans ce dossier, il n’y aura que les tests mais écrits de deux façons :

  • La façon Behat : le langage utilisé par l’homme naturellement (fichiers : *****.feature)
  • La fonction cachée derrière la phrase utilisée (fichiers : ****.php).

Le lien qui relie ses deux tests se situent dans le commentaire inséré au dessus de la fonction dans le fichier ****.php. Cela permet de transférer la transcription humaine du test en langage PHP interprétable pour notre machine.

Petite astuce : certaines fonctions sont déjà intégrées dans le module de Drupal. Par conséquent nous pouvons lister toutes les fonctions grâce à la commande : behat -dl. Cela liste toutes les fonctions déjà disponibles sous le langage de Behat que nous pouvons réutiliser.

Par exemple, voyons ce test simple footer.feature :


Feature: Footer

Scénario : Vérification de la présence du footer sur la homepage
Given I am on the homepage
Then I should see an ".footer" element 

C’est assez simple à lire non ? Tout d’abord on exprime à Behat que l’on veut aller sur la homepage (configuration nécessaire du Behat.yml pour savoir sur quelle adresse il doit aller) pour vérifier ensuite la présence de la classe ‘footer’.

Lorsqu’on lance ce test dans un terminal, voici ce que l’on obtient :

Passons à une fonction plus compliquée nécessitant un développement personnel d’une fonction : la gestion des cookies.


Feature: Utilisation des cookies

  Background: Connection admin établie
    Given I am on the homepage

  Scenario: Je n'ai pas le cookie => message
    When I have not this cookie "g-cookies"
    Then I should see an ".cookie-banner-exist" element

  Scenario: Acceptation du bandeau cookie
    Given I have not this cookie "g-cookies"
    When I should see an ".cookie-banner-exist" element
      And I follow "Accepter"
    Then I have this cookie "g-cookies"
      And I reload the page
      And I have this cookie "g-cookies"
      And I should not see an ".cookie-banner-exist" form element 

Ici nous voyons deux scénarios dans le fichier .feature. J’ai volontairement mis ces deux scénarios dans le même fichier afin de leur définir un contexte commun. On peut voir au dessus des scénarios la présence d’un ‘Background’. Il permet de définir un contexte à tous les scénarios qui se situent dans le fichier. Cela évite donc pas mal de duplications de code.
Dans un deuxième temps, sur la phrase Given I have not this cookie "g-cookies" il y a eu un développement personnel. C’est-à-dire que l’on a dû développer une fonction permettant de vérifier la présence de ce cookie. La voici :


/**
   *
   * @When /^I have this cookie "(?P(?:[^"]|")*)"$/
   */
  public function IHaveThisCookie($cookie) {
    if ($this->getSession()->getCookie($cookie) == NULL) {
      throw new Exception();
    }
  } 

Il s’agit d’une fonction simple en POO (Programmation Orientée Objet), mais qui est bien utile. Cette fonction a en paramètre “$cookie”. Ce paramètre est renseigné via “(?P<text>(?:[^"]|")*)”, nous avons donc le lien entre Behat et les fonctions associées.
Cette fonction s’ajoute au fichier FeatureContext.php afin d’être reconnue par Behat pour qu’elle soit utilisée.
Ainsi nous pouvons ajouter toutes les fonctions souhaitées les unes à la suite des autres (avec des commentaires) pour pouvoir effectuer des tests appropriés.
Voici une liste des fonctions permettant de vérifier pas mal de choses utiles :

  • L’attente d’une réponse AJAX

/**
  * @Then /^I wait for the ajax response$/
  */
 public function iWaitForTheAjaxResponse()
 {
   $this->getSession()->wait(5000, '(0 === jQuery.active)');
 } 
  • Remplir un fichier WYSIWYG dans Drupal :

/**
 * Fills in WYSIWYG editor with specified id.
 *
 * @Given /^(?:|I )fill in "(?P<text>[^"]*)" in WYSIWYG editor "(?P<iframe>[^"]*)"$/
 */
 public function iFillInInWYSIWYGEditor($text, $iframe) {
  try {
  $this->getSession()->switchToIFrame($iframe);
  }
  catch (Exception $e) {
    throw new Exception(sprintf("No iframe with id '%s' found on the page '%s'.", $iframe, $this->getSession()->getCurrentUrl()));
  }
  $this->getSession()->executeScript("document.body.innerHTML = '<p>".$text."</p>'");
  $this->getSession()->switchToIFrame();
}
  • La suppression de chaque noeud créé pour les tests par Behat :
/**
   
   * Remove any created nodes.
   *
   * @AfterScenario
   */
  public function cleanNodes() {
  // Remove any nodes that were created.
  foreach ($this->nodes as $node) {
    $this->getDriver()->nodeDelete($node);
   }
   $this->nodes = array();
  }
  • L’affichage du code HTML de la page s’il y a une erreur dans les tests :

 /**
   *
   * @AfterScenario
*/
  public function printLastResponse()
  {
   echo (
    $this->getSession()->getCurrentUrl()."nn".
    $this->getSession()->getPage()->getContent()
   );
  }

En résumé les tests fonctionnels avec Drupal 8 nous permettent de vérifier, à tout instant, ce qui a été prévu avec le client fonctionnel. Ce qui permet au développeur de vite relever les problèmes lors de l’exécution de ces tests (à chaque push sur une branche particulière, par exemple) et de les corriger. Malgré une documentation assez fluide et lisible, la réalisation de ces tests est assez chronophage… Il faut compter en moyenne 20% de temps de développement en plus sur une tâche pour y ajouter des tests.  Mais ces 20% de temps supplémentaires sont en général bénéfiques pour la suite du projet. Plus un projet est grand, plus le besoin de tests se fait sentir.

Crédit photo : David Travis