UPDATE: Un billet plus récent propose une solution plus évoluée: [Test] Cacique + Selenium IDE +  Selenium-Grid : pour des tests automatisés et industrialisés


Le développement d'application web ou de site web nécessite le recours à des tests automatisés. Ce sujet à déjà été largement abordé dans ce blog (voir le cadre "lisez également").  On avait déjà présenté Selenium. Toutefois, cet outil seul n'offre pas la satisfaction attendue pour des tests de non-régression. Plusieurs limitations frustrantes de cette solution rendent l'utilisation basique inutile pour un projet. Il faut donc aller plus loin.

Selenium & PHPUNIT

Pré requis : Installation

Pour reproduire les manipulations de cet article il convient d'installer Selenium IDE, Selenium RC et php-unit.

Enregistrement du test avec Selenium

Pour illustrer ce billet, on se basera sur le test de la fonction de dépôt d'un commentaire sur la page "Bac à sable" de ce blog.
Le scénario de test est le suivant : de la page d'accueil, on clique sur le lien "Bac à sable" dans la colonne de droite, on complète le formulaire de commentaire et on le valide.
Dans Firefox, via le menu Outils -> "Selenium IDE"on lance l'add-on. La fenêtre Selenium s'ouvre. Par défaut, l'enregistrement se lance automatiquement. Attention, il est important de bien initialiser le test en indiquant l'url sur laquelle le site commence. Pour cela, il faut (taper l'url dans la barre d'adresse et) valider avec la touche "Enter" du clavier.
selenium-ide-test-record.png
Selenium va enregistrer toutes les action sur l'onglet du navigateur:
- les liens qui sont cliqués
- les textes saisis
- le chargement d'une page est détectée pour insérer un temps d'attente
Selenium a besoin d'une intervention de l'utilisateur pour enregistrer les assertions. Les assertions sont les expressions (les tests) que l'on soumet pour le test. Dans le cas présent, la première assertion vérifie la présence du lien "Bac a sable".
selenium_verify_text.jpg
Parmi les possibilités, on peut aussi enregistrer une assertion pour tester la présence d'une balise html à partir de son id:
selenium_verify_elem.jpg
Au final, on obtient l'enregistrement suivant:
selenium_result.png
Cet enregistrement peut être rejoué à l'infini par le plug-in qui va vérifier les assertions et lever une erreur si l'une d'elles n'est pas vérifiées. A ce stade, l'intérêt de la chose est assez faible. Certes, on peut enregistrer des parcours et faire des assertions mais on est obligé de répéter l'enregistrement (ou de réécrire le script) pour chaque données à tester. De plus, on ne peut réaliser ce test qu'avec Firefox, pas avec d'autres navigateurs.

Pour aller plus loin, il faut compenser les limitations de l'extension Firefox en passant par php-unit.
La grande force du module Firefox Selenium IDE s'est sa capacité à exporter l'enregistrement dans un format PHP/phpunit.
selenium_export_php.jpg

Le code exporter en php est le suivant :
<?php

require_once 'PHPUnit/Extensions/SeleniumTestCase.php';

class Example extends PHPUnit_Extensions_SeleniumTestCase
{
  protected function setUp()
  {
    $this->setBrowser("*chrome");
    $this->setBrowserUrl("http://change-this-to-the-site-you-are-testing/");
  }

  public function testMyTestCase()
  {
    $this->open("/");
    try {
        $this->assertTrue($this->isTextPresent("Bac à sable"));
    } catch (PHPUnit_Framework_AssertionFailedError $e) {
        array_push($this->verificationErrors, $e->toString());
    }
    $this->click("link=Bac à sable");
    $this->waitForPageToLoad("30000");
    $this->type("c_name", "test");
    $this->type("c_mail", "test@test.fr");
    $this->type("c_content", "Ceci est un commentaire posté automatiquement");
    try {
        $this->assertTrue($this->isTextPresent("Nom ou pseudo"));
    } catch (PHPUnit_Framework_AssertionFailedError $e) {
        array_push($this->verificationErrors, $e->toString());
    }
    try {
        $this->assertTrue($this->isElementPresent("c_name"));
    } catch (PHPUnit_Framework_AssertionFailedError $e) {
        array_push($this->verificationErrors, $e->toString());
    }
    try {
        $this->assertTrue($this->isElementPresent("c_mail"));
    } catch (PHPUnit_Framework_AssertionFailedError $e) {
        array_push($this->verificationErrors, $e->toString());
    }
    try {
        $this->assertTrue($this->isElementPresent("c_site"));
    } catch (PHPUnit_Framework_AssertionFailedError $e) {
        array_push($this->verificationErrors, $e->toString());
    }
    $this->click("preview");
    $this->waitForPageToLoad("30000");
    try {
        $this->assertTrue($this->isTextPresent("Ceci est un commentaire posté automatiquement"));
    } catch (PHPUnit_Framework_AssertionFailedError $e) {
        array_push($this->verificationErrors, $e->toString());
    }
    $this->click("//form[@id='comment-form']/fieldset[2]/p/input[2]");
    $this->waitForPageToLoad("30000");
  }
}
?>
On s'en sort avec quelques instructions simples
  • click pour cliquer sur un lien
  • waitForPageToLoad pour attendre le chargement de la page
  • type pour saisir du contenu dans un champs
  • isTextPresent teste la présence d'une chaine de caractère
  • isElementPresent teste la présence d'un élément (div ou autre balise)
Avant d'aller plus loin, il est important de comprendre comment exploiter ce code exporté. Comment ça marche ?


Exécuter le test

C'est à ce moment qu'on met en œuvre un peu de logistique. Il faut
Lancer le serveur java :  java -jar selenium-remote-control-1.0.3/selenium-server-1.0.3/selenium-server.jar
lancer phpunit en ligne de commande : phpunit test_blog_nalis.php

$ phpunit test_blog_nalis.php
PHPUnit 3.4.13 by Sebastian Bergmann.

E

Time: 7 seconds, Memory: 5.75Mb

There was 1 error:

1) Example::testMyTestCase
PHPUnit_Framework_Exception: Response from Selenium RC server for testComplete().
XHR ERROR: URL = http://change-this-to-the-site-you-are-testing/ Response_Code = -1 Error_Message = Request Error.


/home/lasselin/Bureau/selenium-n/test_blog_nalis.php:15
/home/lasselin/Bureau/selenium-n/test_blog_nalis.php:15

FAILURES!
Tests: 1, Assertions: 0, Errors: 1.

Pas de panique. Cela ne fonctionne pas "out of the box". En effet, selenium-rc a initialisé le site sur : "http://change-this-to-the-site-you-are-testing/" et le navigateur à "chrome".

Attention, le nom du fichier doit correspondre au nom de la classe. La classe "Example" généré par défaut doit donc se trouver dans un fichier Example.php.

Ce script est peu satisfaisant. En effet, on a 1 scénario de test avec différent "test" (des assertions) mais:
  • un seul navigateur utilisé
  • une granularité de résultat très haut niveau: phpunit s'arrête à la première anomalie.
  • un seul jeu de données
  • les cas d'erreurs nécessitent un scénario spécifique
  • la sortie est peu verbeuse
Pour obtenir une stratégie de test efficace, il faut intervenir sur ces 3 éléments.

Vers une stratégie de test efficace ?


Les éléments de solutions exposés ci-après sont ceux que j'ai expérimenté faute d'avoir trouver mieux (mais si vous avez une solution plus élégante ... partagez la avec un commentaire !)

Un seul navigateur utilisé ? -> Configurer plusieurs navigateurs

Il suffit de préciser les navigateurs à employer :
public static $browsers = array(
        array(
        'name'    => 'Googlechrome',
        'browser' => '*googlechrome /usr/lib/chromium-browser/chromium-browser'
        ),
        array(
        'name'    => 'Firefox',
        'browser' => '*firefox'
        ),
        array(
        'name'    => 'Internet Explorer',
        'browser' => '*iexploreproxy'
        ),
        array(
        'name'    => 'Opera',
        'browser' => '*opera'
        ),
        array(
        'name'    => 'Safari',
        'browser' => '*safariproxy'
        )
    );


A l'exécution, le script lancera les navigateurs séquentiellement. Attention "*chrome" n'est pas Google chrome. Dans l'exemple, on trouve une configuration spécifique du chemin de chrome pour linux.

Enchainer après une anomalie -> Débrancher les exceptions

phpunit s'arrête à la première anomalie car il effectue un try / catch pour chaque assertion
try {
        $this->assertTrue($this->isElementPresent("c_site"));
    } catch (PHPUnit_Framework_AssertionFailedError $e) {
       array_push($this->verificationErrors, $e->toString());
    }

et le catch aboutit à l'arrêt du scénario.
Le fonctionnement de Selenium pose une autre contrainte. L'exécution de chaque cas de test suppose le relancement du navigateur (avec initialisation de la session selenium) ceci rallonge déraisonnable la durée du test.
La solution est d'encadrer non pas l'assertion mais l'ensemble du scénario (et de tout le jeu de donné).
Inconvénients:
-  si un pas échoue, il faut attendre le timeout.
- phpunit ne renvoi plus le résultat de tests.

Utiliser des jeux de données -> Variabiliser

Si l'enregistrement décrit bien un cas passant, il n'est exécuté qu'avec un seul jeu de données. Ors, il est nécessaire de réaliser des tests sur un panel de données plus étendu. Il est également nécessaire de compléter le scénario actuel par des scénarios de cas non-passant pour vérifier que les erreurs sont bien gérées (exemple: champ commentaire vide, champ email sans "@").
Il est préférable que le jeu de données soit extérieur au script PHP. Il apparait souvent (c'est préférable) que les personnes qui écrivent les tests ne sont pas celles qui développent. A plus forte raison, quand les tests sont écrit par des non-informaticiens. Dans ce cas, il est nécessaire de choisir un format de fichier facilement utilisable par des clients, des spécificateurs, des intervenants dont le métier n'est pas l'informatique (mais celui de l'application en développement). Les jeux de tests réalisés par ces personnes n'en seront que plus pertinents.
PHP lit nativement les fichiers ini (avec parse_ini_file()) mais éditer un fichier texte avec une syntaxe même simple va représenter un frein pour certaines personnes. Le fichier csv est une meilleur option. J'entends d'ici:  "le format avec les virgule ou les points virgules, une meilleur option ?". Oui, car les fichiers csv sont manipulables avec les tableurs openoffice ou office (plus généralement utilisé par les clients). Il est alors facile pour eux de modifier ou d'ajouter des cas de tests.
Pour alimenter les scénarios en donnés on utilise le mécanisme de "provider" de phpunit.

    public function provider()
    {
        if (($handle = fopen("data-test_comment.csv", "r")) == FALSE) die();
        $data = fgetcsv($handle, 1000, ";",'"');
        $i=0;
        while($data = fgetcsv($handle, 1000, ";",'"'))
        {
            if($data) $datas[$i++]=array($data);
        }
        return $datas;
    }

Pour indiquer à phpunit quelle est la fonction alimentée par ces données, on ajoute le commentaires:
    /**
     * @dataProvider provider
     */

avant la ligne:
     public function testMyTestCase($datas)
On y déclare le paramètre datas qui contient les données d'une ligne du csv.

Vérifier la gestion des erreurs -> Cas passant et non passant

Via le code PHP on peut aiguiller le scénario de tests vers telle ou telle vérification.
Dans le cas présent on peut préciser dans une colonne qu'on génère un cas d'erreur (booléen à 1 pour une erreur $datas[3]  dans cet exemple) et préciser le message attendu ($datas[2]) pour que Selenium test sa présence.

if ($datas[3] == 1)
                    {
                        $this->assertTrue($this->isTextPresent($datas[2]));
                    }
                    elseif ($datas[3] ==0)
                    {
                        $this->assertTrue($this->isTextPresent($datas[4]));
                    }


Un rapport de test -> Compléter la sortie d'information

Pour les mêmes raisons, offrir un récapitulatif des tests au format CSV facilitera le reporting des tests. Ce reporting permet de suivre le pourcentage de tests réussi pour un scénario donné.
On peut aussi extraire le texte afficher (en fonction de l'id de la balise html) pour l'afficher dans le log de sortie.
$common_data=date(DATE_ATOM).";".$datas[0].";".$datas[1].";".$datas[2].";".$datas[3].";".$datas[4];
fputs($log_file, $common_data.";".";OK;".$this->getText("pr")."\n");


Inconvénient: Je ne suis pas parvenue à trouver un moyen de loguer le navigateur en cour d'utilisation.
On pourrait parcourir le tableau des navigateurs, mais lors d'une échec, l'incrémentation n'est pas exécuté ce qui aboutit à un log incohérent.

Bonus

Durant l'exécution du test, php-unit lance les navigateurs. Les navigateurs lancés dans ce contexte ne chargent pas le profil de l'utilisateur courant. Il en résulte parfois des fenêtres de navigateurs très réduites qui rendent impossible le suivi de l'exécution des tests. C'est malheureusement courant avec Firefox
A cette fin, on peut commencer le test par le chargement d'une page d'initialisation qui va forcer en javascript les dimensions de la fenêtre. Toutefois, cette possibilité se heurte à certains paramètres de sécurité qui empêchent le chargement d'une page d'un autre domaine que celui du test. Cela oblige à disposer de la page en question sur le domaine testé.

Résultat

selenium_resultat.png


On obtient une solution permettant d'effectuer des tests de non-régression grâce à l'automatisation des actions sur le navigateur web. Toutefois, cette solution, si elle commence à répondre à des attentes de tests industrielles, reste insatisfaisante. Plusieurs inconvénients rendent pénibles l'utilisation de phpunit/selenium. La difficulté d'identifier la raison d'une anomalie, l'impossibilité d'obtenir certaines informations en sortie sont autant de complication qui rendent les tests de non-régression anormalement long. Vos commentaires sont les bienvenus.