Tags

, , ,

Les tests unitaires classiques sont utilisés pour tester une partie du code d’une application, dans un environnement d’exécution bien maîtrisé. Même si les tests de ce genre sont nécessaires, et que leurs bénéfices ne sont plus à démontrer, il est également important de pouvoir réaliser de véritables tests fonctionnels d’une application web, en condition réelle, c’est à dire déployée sur un serveur d’application et en la testant depuis un navigateur. Selenium est un des outils les plus utilisés pour réaliser ce genre de tests. Après une présentation rapide de cet outil, cet article a pour but de vous exposer mon retour d’expérience sur son utilisation au quotidien.

Selenium est en fait un ensemble d’outils, dont les plus importants sont :

  • Selenium IDE : plugin pour Firefox qui permet d’enregistrer et de rejouer des commandes utilisateur sur une application web – ouverture d’une URL, click sur un bouton, entrée d’une valeur dans un champ, etc. Ces commandes peuvent être exportées sous la forme de tests unitaires dans différents langages (HTML, Java, C#, etc.)
  • Selenium Core : bibliothèque JavaScript, compatible avec un grand nombre de navigateurs, utilisée pour lancer les commandes utilisateur sur l’application web. Cette bibliothèque peut être utilisée directement pour lancer les tests Selenium codés en HTML.
  • Selenium Remote Control : application serveur qui permet l’exécution des tests unitaires Selenium codés en Java, .Net, Perl, PHP, Python ou Ruby et se reposant sur une bibliothèque cliente.

Le schéma suivant présente l’architecture de fonctionnement du Selenium Remote Control :
Architecture de fonctionnement Selenium Remote Control
Voici un exemple de test Selenium codé en Java/jUnit, qui reproduit une recherche sur Google de “blog proxiad” et qui vérifie que le premier résultat retourné est bien le site sur lequel vous surfez actuellement :

public class BlogProxiadTest extends SeleneseTestCase {
    public void setUp() throws Exception {
        setUp("http://www.google.fr/", "*iexplore");
    }
    public void testRechercheGoogle() throws Exception {
        selenium.open("/"); // appel de la page d'accueil de Google
        selenium.waitForPageToLoad("30000"); // attente du chargement complet de la page
        selenium.type("q", "blog proxiad"); // critère de recherche
        selenium.click("btnG"); // click sur le bouton "Recherche Google"
        selenium.waitForPageToLoad("30000");
        assertEquals("ProxiAD vous parle d'IT", selenium.getText("//div[@id='res']/div/ol/li[1]/h3/a")); // titre de la page du 1er résultat
        assertEquals("blog.proxiad.com/ -", selenium.getText("//div[@id='res']/div/ol/li[1]/div/cite")); // URL de la page du 1er résultat
        selenium.click("link=ProxiAD vous parle d'IT"); // click sur le lien
        selenium.waitForPageToLoad("30000");
        verifyTrue(selenium.isTextPresent("EDITO")); // l'édito doit être présent sur la page d'accueil
    }
}

Le code de base du test a été généré par Selenium IDE, puis modifié de la façon suivante :

  • ajout de commentaires
  • ajout d’une temporisation après le clic sur le bouton de recherche : la commande waitForPageToLoad n’est pas toujours générée par Selenium IDE
  • retouche des expressions XPath, de façon à correctement référencer la première ligne de résultat de la recherche, car la pub Google, lorsqu’elle apparaît dans la marge droite, rend caduc les expressions XPath générées par Selenium IDE

En général, le code généré par Selenium IDE n’est pas utilisé tel quel. On doit parfois exécuter des traitements avant et après chaque test : affichage de la page d’accueil de l’application, clic sur un bouton “Déconnexion”, etc. On a parfois besoin également d’accéder à la base de données pour récupérer certaines informations ou faire des assertions, et il convient de les réaliser au sein du test Selenium, au moment où l’on en a besoin, et en prenant soin de ne pas être dans un contexte transactionnel, sans quoi le niveau d’isolation empêcherait de voir les données modifiées par l’application.

La mise en œuvre de Selenium sur une application web nécessite très souvent des adaptations ou des petites corrections de l’application, mais on peut voir ces modifications comme des améliorations de l’existant. Par exemple, à moins de pouvoir s’en passer, il faut que l’application testée puisse fonctionner sous Firefox, puisque Selenium IDE ne fonctionne que sous ce navigateur. Cela nécessite parfois de réécrire certains codes JavaScript, qui peuvent présenter une incompatibilité, mais ces corrections sont souvent mineures.

Autre exemple : la localisation d’un élément sur une page de l’application n’est parfois possible qu’avec une expression XPath référençant la position exacte d’un élément HTML, du type /html/body/table/tbody/tr/td/div/div[2]/ul/li/ul/li[8]/a. A chaque modification de la page testée, ce genre d’expression XPath n’est plus forcément valide. C’est pour cela qu’il faut modifier les pages testées pour que les recherches dans l’arbre DOM soient facilitées : ajout d’identifiants sur les éléments remarquables de la page, utilisation de la balise alt pour identifier les images ou certains liens, etc.

Selenium est très complet et peut s’adapter à la majorité des applications web, y compris celles qui utilisent des frames, des fenêtres pop-up ou même de l’AJAX, mais l’expérience montre qu’il faut souvent retoucher le code généré pour que le test fonctionne correctement : ajout de commandes waitForPageToLoad, selectFrame, selectWindow ou, pour les pages utilisant AJAX, les commandes waitForXxx ou waitForNotXxx. Il y a cependant certains cas particuliers qui ne sont pas gérés, et qui peuvent bloquer complètement le test, comme les fenêtres d’alertes provenant du navigateur, les alertes JavaScripts générées depuis l’évènement onLoad de la page, ou encore les uploads de fichiers qui ne sont pas supportés, mais des solutions de contournement sont possibles.

Enfin, il est possible d’intégrer l’exécution des tests Selenium dans un processus d’intégration continue, pourvu que l’on arrive à automatiser toutes les étapes nécessaires : construction des livrables applicatifs, déploiement de ces livrables sur le serveur d’application, lancement des tests Selenium contre l’application déployée. En pratique, de nombreux outils sont capables de faire cela (par exemple Maven avec le plugin Selenium pour la gestion du Selenium Remote Control, et le plugin Cargo pour le déploiement sur le serveur d’application). Mais on s’aperçoit assez vite que l’ensemble de ce processus est très lourd, et que les tests Selenium s’exécutent très lentement par rapport à de véritables tests unitaires du code de l’application. C’est pour cela qu’en général on se limite à une exécution par jour de ces tests. Pour information, il existe plusieurs moyens de paralléliser l’exécution des tests Selenium, et de gagner en rapidité d’exécution : Selenium Grid pour la multiplication des instances de Selenium Remote Control, exécution multi-threadée des tests Selenium, mais il faut dimensionner l’infrastructure de test en conséquence.

Dans un prochain article, je vous présenterai Tellurium, un fork de Selenium qui ajoute de nouvelles fonctionnalités et qui semble prometteur.

Advertisements