La jouer KISS avec Selenium

le 17/12/2013 par Armand Ladevèze
Tags: Software Engineering

Tester une application est un sujet soulevant de nombreuses problématiques, bien résumées par le schéma de Brian Marick ci-dessous.

Ainsi, dans un projet industrialisé, les best-practice de testing appellent plusieurs typologies de tests automatisés à se côtoyer : des test unitaires (TU), des tests fonctionnels, et éventuellement des tests passant par l'IHM.

Cette richesse est essentielle à l'amélioration de la qualité de nos applications, mais nécessite l'usage de nombreux outils.

Là où les outils de tests unitaires, de mocking,  et de tests fonctionnels sont des références, les outils de tests passant par l'IHM ne sont que peu maîtrisés, en marge des connaissances de l'équipe de développement.

Ils ont tendance à être la 5e roue de la charrette.

C'est le cas de Selenium.

Cet article est un retour d'expérience présentant une implémentation bien éloignée des best practices de l'outil, simple et pragmatique.

Avant-propos : La place de Selenium

Cet article ne traite pas de la place de Selenium vis-à-vis des autres outils de testing, mais bien d'une façon d'utiliser l'outil.

Il n'est pas question d'utiliser Selenium comme seul outil de test automatique, il est un complément à l'écosystème de tests automatisés classique.

Cet article situe Selenium dans un contexte de "Smoke Test", à savoir une vérification du caractère opérationnel des grandes fonctionnalités d'un site. Une confirmation que tout est bien assemblé sur l'environnement de test, utilisable avec un navigateur, prêt pour une recette utilisateur.

Etat des lieux

Le projet de refonte de l'espace client d'une banque privée (de taille PME) avait démarré un an auparavant, avec une stack technique bien remplie, en méthodologie Scrum/Kanban.

L'environnement technique : .Net (C#, MVC), du BackBoneJs, TFS

Méthode : Scrum / Kanban

Les outils de test automatisés :

Test unitaires (520 tests) : MST (Moq pour le mocking)

BDD (420 tests) : Jasmine pour la couche BackBoneJs

Tests fonctionnels (300 tests) : GreenPepper

Tests IHM (20 tests) : Selenium.

Les tests Selenium étaient codés en C#, techniquement comme des tests unitaires, et présents dans un projet de test à part.

L'exécution des tests Selenium était porté par un build spécifique, exécuté toutes les nuits, assurant une exécution régulière et historisée. Les bugs étaient détectés au pire tous les matins.

Dès lors qu'on était sur du C#, les développeurs avaient beaucoup de possibilités pour adapter les tests à leur besoin et étaient dans un langage maîtrisé.

Les éléments communs aux tests étaient factorisés en sous-unités fonctionnelles. Par exemple, une classe automatisait la saisie des identifiants de connexion et la validation du formulaire. Tous les tests touchant à la partie sécurisée de l'espace client faisaient appel à cette classe. Cela facilitait la maintenance.

Les tests pouvaient être exécutés sur n'importe quel navigateur et se basaient sur du Selenium2 (WebDriver).

On était sur une belle implémentation ! Clairement dans les recommandations des concepteurs de Selenium.

Le constat

Mais pourtant, après un an de projet on rencontrait plusieurs douleurs :

  • Les tests étaient longs à s'exécuter depuis VisualStudio, le debug était pénible.
  • Ecrire un test qui marche prenait du temps, et correspondait souvent à du pas à pas pour chaque commande.
  • L’exécution de l’ensemble des tests était très longue (15 minutes), pour une dizaine de tests
  • Les tests échouaient régulièrement durant le build d'intégration, sans que la raison soit claire (échec car le site n'avait pas répondu à temps aux commandes, dysfonctionnement ponctuel du serveur intégration, …) .
  • L'apport des tests ne paraissait pas évident à l'équipe, surtout compte tenu du coût associé

Au final, les tests Selenium furent sortis du build et laissés à l'abandon, pour le plus grand soulagement de l'équipe.

Quelques exemples de Verbatim : "Je compile, je lance le test, qui lance le navigateur, qui assimile les commandes, etc. 2 minutes pour atteindre la page à tester" "On a déjà des tests unitaires, du GreenPepper, du Jasmine, ça commence à faire beaucoup !" "Oh non, les tests ont planté cette nuit ! Qui regarde ?"

Mécontents de ce constat, nous avons tenté une autre approche, pour donner une deuxième chance à l'outil.

Une deuxième chance

Elle s'articula autour d'un POC, piloté par trois axes : Simplicité, stratégie et organisation.

Simplicité de création/maintenance

Les douleurs constatées n'étaient pas sur le principe de faire évoluer les tests avec l'application, mais bien sur la lourdeur de développement des tests. Difficiles à écrire, lents à s'exécuter.

Pour faire simple, on choisit d'utiliser SeleniumIDE.

SeleniumIDE est un plugin Firefox, petit IDE qui permet d'enregistrer des tests, les modifier, les exécuter, les débugguer etc.

Si l'enregistrement automatique du test est perfectible, il constitue un premier support pour créer un test.

L'exécution d'un test est très rapide, car :

  • On ne compile pas les tests
  • Le test n'a pas à appeler l'API WebDriver pour chaque commande d'un test, qui la transcrirait en appel au navigateur
  • On n'a pas besoin d'attendre que le démarrage du navigateur à chaque test

Directement lié à Firefox, le plugin exécute les commandes immédiatement.

Sur un exemple très simple (faire une recherche Google et accéder à un des résultats), on est sur du 15s en C# contre 5s avec SeleniumIDE. Même si on ne s'appuie pas ici sur un benchmark détaillé, la différence est visible.

L’IDE n’est pas parfait, mais le confort d’utilisation est là.

Il reste adapté à du développement rapide d'un test, que l'on modifie facilement à chaud, pour le passer encore et encore pour roder le test.

Sans compter les nombreuses extensions, le plugin propose quelques outils pratiques, comme une aide à la construction de locator, une illustration visuelle de l'élément ciblé sur la page, un accès immédiat à la doc des commandes, etc.

En deux jours, un ensemble de test était opérationnel couvrant la quasi-totalité des  fonctionnalités du site. (une vingtaine)

Ils s'exécutaient en moins de 3 minutes.

SelIDE

Interface du plugin SeleniumIDE

Stratégie de testing - Cohérence du cahier de test

Selenium permet de tester beaucoup de choses dans les pages Web : Le déroulement des scénarios utilisateurs, la présence de tel ou tel élément, le déclenchement d'une règle de gestion, les contrôles de surface, etc.

Un anti-pattern classique est de vouloir tout tester, et finalement d'essayer de remplacer la recette par les tests Selenium.

Pour définir cette stratégie, plusieurs questions se posent :

  • Quel est l'objectif des tests ?

Une question essentielle.

Avant de savoir comment faire les tests, il faut fixer ce qu'ils vont apporter. Un principe directeur de test, sur lequel on pourra s'appuyer pour mettre en balance le coût du test (réalisation + maintenance) et le gain du test.

Le gain du test est ce que son succès/échec nous apprend sur l'application.

Au moins un utilisateur peut il vraiment se logguer ? Des tests unitaires nous indiqueraient seulement que les différents composants d'authentification sont OK.

Sur cette mission, nous avons produit des tests de non régression, sur un périmètre d'un test par fonctionnalité majeure du site.

Par exemple : "Un client effectue un versement en ligne sur tel type de contrat", ou "Un utilisateur utilise le formulaire de contact pour envoyer un email à son conseiller"

  • Comment est structuré le plan de test ?

La structuration du plan de test fut basée sur une contextualisation des tests,  pour maximiser la vitesse d'exécution de l'ensemble des tests.

Les tests sont jouables uniquement dans un contexte précis. Par exemple, le test du versement part du principe que le navigateur affiche déjà la page d'accueil d'un client loggué sur le site. Le test déroule le versement et retourne sur la page d'accueil.

Le test suivant déclenche le processus d'arbitrage, qui démarre aussi sur la page d'accueil, et ainsi de suite.

Certains tests étaient donc dépendants d'autres tests, l'enchaînement étant orchestré par la suite de tests.

Cette inter-dépendance n'était pas systématique. Finalement, seul le test de login était critique : S'il échouait, le reste des tests échouaient dans la foulée, nous remontant que l'application était totalement KO. Un retour un peu exagéré, mais qui s'approchait assurément du retour qu'aurait fait des utilisateurs sur une même constatation.

L'ensemble des tests était finalement assez proche de ce que ferait un testeur pressé : Il se loggue avec le client, et effectue autant de scénarios que possible dans ce contexte.

testSuite

Exemple d'une suite de test ordonnée (login, scénarios, déconnexion)

  • Quelle granularité pour ce qui est testé ?

Pour valider une fonctionnalité, le test correspond à l'automatisation d'un scénario utilisateur sur le site.

Dit autrement, le test est réussi si l'enchaînement des étapes du scénario s'est déroulé sans heurts.

Mais ce n'est souvent pas suffisant.

Arrivé à la dernière étape d'un formulaire, le site peut notamment afficher à l'utilisateur une information décrivant la réussite de son action. ("Votre versement a bien été effectué")

Il convient alors que le test Selenium scrute la présence de cette information sur la page.

Cette nécessité de tester des éléments dans les pages amène la fameuse question : "Jusqu'à quel point on teste ce qui s'affiche ?"

Dans la mission, on a commencé par tester le minimum, à savoir seulement si on arrive bien sur la bonne page du formulaire, et que la donnée essentielle de la fonctionnalité est présente. La présence d'un texte, rien de plus.

Il fallait être vigilant pour ne pas dévier vers l'automatisation de la recette, ou de faire doublon avec les tests fonctionnels.

Industrialisation Vs Organisation

On a choisi de ne pas industrialiser l'exécution des tests, mais de les intégrer dans notre méthode de travail.

Moins d’industrialisation….

Nos tests sont enregistrés dans le contrôleur de sources, dans leur format d'enregistrement (Html).

Ils sont directement utilisables depuis le plugin de n'importe quel membre de l'équipe, qui peut jouer les tests sur l'url de son choix (son poste, le serveur d’intégration, etc.)

Utiliser des tests au format de SeleniumIDE (Html) n'empêche pas de les intégrer dans un build.

Il y a deux solutions envisageables :

  • Exécuter en ligne de commande le serveur SeleniumRC sur la suite de test ( C'est une fonctionnalité obsolète, basée sur l'option -htmlSuite de Selenium1).
  • Exporter les tests Html en code (C#, Java, …), afin de les intégrer proprement dans un build.

Aucune de ces deux approches n’a été mises en place, car:

  • Elles sont soit obsolètes, soit lourdes (doublons de tests)
  • Elles nécessitent un effort de paramétrage des environnements de test
  • Elles nécessitent une gestion sans faille des temporisations dans les tests
  • En cas d’erreur, il y a vraiment peu d’éléments de debugging
  • Dès lors que toutes les couches de l’application et son environnement d'hébergement sont testées, la probabilité d’un bug non reproductible n’est pas négligeable. Ce type de bugs nuit fortement à l’appréciation de l’outil

Malgré l'écriture de tests aussi robuste que possible ("waitForElement" avant d'appeler une commande sur un élément, Xpath évolué avec du contains, …), il arrive couramment d'avoir un test où il manque une temporisation, générant un bug en apparence inexplicable.

S'en apercevoir sur son poste en cours d'usage est assez différent de constater l'échec surprise d'une build d'intégration pourtant déjà passée précédemment. Ce genre de constatation dégrade fortement la confiance en l'outil.

…Plus d’organisation

Lors de la revue de code d'une story, le relecteur exécute les tests et constate une éventuelle régression.

Le Definition Of Done entre les colonnes "En cours" et "Fait" impose une exécution réussie des tests Selenium.

L'exécution des tests manuellement implique le relecteur, qui devra analyser l'échec et résoudre le sujet, éventuellement avec le développeur de la story en cours de relecture.

Egalement, pour le rituel de fin d'itération, les tests sont joués pour confirmer la viabilité du package d'itération, avant la démonstration et/ou la livraison.

6 mois après - Maintenance et Evolution des tests

Une fois un premier ensemble de tests couvrant l’application, la question était de les faire évoluer, conjointement aux développements de nouvelles stories.

La stratégie de test a été adaptée à l'évolution des fonctionnalités du site, ainsi que la méthode de travail.

Ainsi, si la story en cours de développement a un impact notable sur les fonctionnalités testées, il faut ajouter/modifier des contrôles côté Selenium.

La plupart des stories ne nécessitent pas de toucher à Selenium, à moins qu'elles impactent notablement une fonctionnalité majeure, qu'elles en créent une nouvelle, ou qu'elles modifient franchement l'interface du site.

Ca c'est le principe.

Dans notre contexte, notre outil de couverture fonctionnelle, GreenPepper,  ne pouvait pas atteindre la partie Javascript de notre application. Cette partie étant assez développée, notre implémentation des tests GPP était faible (Seuls 35% des stories appelaient du code).

Insidieusement, cet état de fait nous a amenés à ajouter des contrôles dans nos tests Selenium.

Après tout, les tests restaient très robustes, et amenait de la valeur.

Donc nos tests ont un peu déviés vers des tests de recette fonctionnelle.

Ce n'est pas l'approche que propose cet article, mais c'est un risque inhérent à l'outil.

Une limite d'utilisation de Selenium pourrait s'évaluer sur un nombre maximum de tests (100 par exemple), ou plus pragmatiquement sur une durée d'exécution (3-5 minutes).

Notre déviance ne nous avait pas fait dépasser ces limites.

Outre cet égarement, plusieurs points furent à retenir :

  • A l’instar d’autres types de test, il faut faire de la revue et refactorer
  • Ecrire dans le marbre (à défaut sur un gros post-it) les grandes règles pour la réalisation des tests.
  • Ne tester que le nécessaire !
  • Toujours vérifier de la façon la plus simple
  • Pérenniser les tests plutôt que les étendre
  • Un test par fonctionnalité, voire un test principal et plusieurs secondaires
  • Si une part du parcours utilisateur est déjà couverte par un test, il ne faut pas la recoder
  • Attention aux paramètres  et valeurs « en dur » dans les tests

Conclusion

Cette refonte fut une réussite, améliorant la productivité et remotivant l'équipe sur l'outil.

Même si cette approche n'est pas industrialisée, elle est bien adaptée au créneau de Selenium, à savoir le complément IHM aux tests fonctionnels. A la fois pour faciliter le travail sur des tests par définition moins pérennes, mais aussi pour limiter la lourdeur d'une automatisation de navigateur Web.

Pour un nouveau projet projet, nous partirions à priori directement sur l'approche décrite ici, plutôt que sur le standard habituel (C# et build).

Il y aura toujours moins de tests Selenium sur les projets que de tests unitaires ou fonctionnels, donc autant simplifier au maximum leur gestion.

Dans le cadre d'une application contenant une bonne part de Javascript en plus de code métier classique, les outils de tests fonctionnels classiques semblent incomplets pour faire office de référentiel commun de test.

On pourrait imaginer étoffer les tests Selenium pour parer à ce manque, mais il faudrait compter sur la présence d'une solide compétence sur l'outil et d'un investissement plus conséquent.

Même si ce n'est pas le sujet de cet article, la croissance des frameworks JS pourrait bien amener cette question à se poser de plus en plus souvent.