Testing automatisé avec Selenium et Python

Nous allons voir sur cette page comment automatiser l'exécution de tests fonctionnels grâce à Selenium et son API pour Python.

Selenium : présentation de l'outil

Selenium est un outil d'automatisation de navigateur web. Il permet donc d'écrire, de manière plus ou moins assistée, des scripts dont l'exécution réalisera automatiquement des actions dans un navigateur web : visiter une page, cliquer sur un lien, remplir un formulaire, etc. et de récupérer les résultats de ces actions.

Créer un script Selenium

Enregistrement via Selenium IDE

La manière la plus simple de créer des scripts Selenium est de les enregistrer à la manière de macros, via l'outil Selenium IDE, un plugin pour Firefox qui permet d'enregistrer les actions que vous réalisez dans le navigateurs.

Selenium IDE
Selenium IDE : un outil pour enregistrer des scripts Selenium très simplement

Nous allons utiliser Selenium IDE pour réaliser quelques actions de navigation sur le site cesi.fr :

  1. Se rendre sur la page d'accueil
  2. Saisir des données de recherche de formation en renseignant :
    • Un domaine de formation
    • Une localisation
  3. Valider la recherche

Au fur et à mesure de notre navigation sur le site, Selenium IDE enregistre les actions réalisées et les consigne dans son interface :

Selenium IDE record test

Notre macro est enregistrée ! Nous pourrons à présent la rejouer à l'infini. Du reste, elle est stockée en interne sour la forme d'une source XHTML :

Source XHTML
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head profile="http://selenium-ide.openqa.org/profiles/test-case">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="selenium.base" href="http://cesi.fr/" />
<title>New Test</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr><td rowspan="1" colspan="3">New Test</td></tr>
</thead><tbody>
<tr>
	<td>open</td>
	<td>/</td>
	<td></td>
</tr>
<tr>
	<td>select</td>
	<td>id=form_1579</td>
	<td>label=Informatique et systèmes d'information</td>
</tr>
<tr>
	<td>select</td>
	<td>id=form_1582</td>
	<td>label=Arras</td>
</tr>
<tr>
	<td>clickAndWait</td>
	<td>css=input[type=&quot;submit&quot;]</td>
	<td></td>
</tr>
</tbody></table>
</body>
</html>

Réaliser des tests automatisés : l'API Selenium

D'aucuns se seront dit à la lecture du début de ce tutoriel : « OK, c'est sympa, mais tout ceci ne permet pas d'automatiser des tests : même si le navigateur réalise des actions par lui même, il faut quelqu’un derrière l'écran pour vérifier que les résultats obtenus sont conformes aux attentes ». Certes !

C'est à ce moment qu'intervient l'API Selenium : nous allons, en écrivant du code de test, automatiser le navigateur et les vérifications associées.

Note Selenium peut être automatisé dans beaucoup de langage différents grâce à des API dédiées : Java, PHP, Python… Pour cette démonstration, nous allons travailler en Python.

Présentation du use case et des test cases

Nous allons ici nous attarder sur le use case « Rechercher une formation ». Ce use case peut être formalisé de la manière suivante en UML :

Use case recherche de formation CESI
Le cas d'utilisation « Rechercher une formation du CESI »

Une version simplifiée sous la forme de user-story pourrait être spécifiée de la sorte :

EN TANT QUE
Internaute
JE SOUHAITE
Pouvoir recherche une formation du CESI sur des critères de domaine de formation, durée, localisation et nature
AFIN DE
Être en mesure d'optimiser mon parcours utilisateur sur le site CESI en trouvant plus rapidement une formation répondant à mes attentes.

Remarque Notez la puissance d'expression plus importante d'UML, qui nous permet de préciser le caractère obligatoire (stéréotype include) ou facultatif (extend) des use cases associés.

De ce use case découlent un nombre important de test cases (nombre de domaines de formation × nombre de durées+1 × nombre de localisations+1 × 2, soit en l'espèce 4466 tests !). Nous n'allons bien entendu pas tester tous ces cas… Nous allons plutôt commencer par tester le cas général.

Un exemple de cas de test est le suivant, exprimé ici en syntaxe Gherkin :

ÉTANT DONNÉ QUE
Je navigue sur la page d'accueil du site CESI
QUAND
Je saisis une formation dans le cadre de recherche « Former ses équipes »
ET QUE
Je clique sur le bouton « Valider »
ALORS
Une page de résultats de recherche p apparait
ET
Cette page p porte le titre « Votre Résultat, votre recherche »

Un autre exemple de cas de test consiste à vérifier que si un pré-requis n'est pas satisfait, la fonctionnalité réagit correctement :

ÉTANT DONNÉ QUE
Je navigue sur la page d'accueil du site CESI
ET QUE
Je n'ai sélectionné aucun critère dans le cadre de recherche « Former ses équipes »
QUAND
Je clique sur le bouton « Valider »
ALORS
Un message d'erreur « Vous devez sélectionner au moins un critère de recherche » est affiché

Nous allons à présent implémenter les test automatisés permettant d'exécuter ces test cases.

Installation de Selenium et de l'API Python

Pour pouvoir exécuter nos tests, nous devons :

  • Installer le serveur Selenium
  • Installer l'API Python

La procédure d'installation est décrite sur la documentation de l'API Selenium Python.

Écriture du code de test

La grande force de la solution que nous allons mettre en place est que nos tests fonctionnels vont s'intégrer complètement dans la solution de testing unitaire et d'intégration de l'application. Autrement dit, nous allons utiliser la même structure de tests que pour notre tests techniques (unitaires et d'intégration).

Nous commençons donc par créer notre classe de test :

tests.py
import unittest
from selenium import webdriver

class TestCesiSearch(unittest.TestCase):

    def setUp(self):
        self.driver = webdriver.Firefox()

    def tearDown(self):
        self.driver.close()


if __name__ == '__main__':
    unittest.main()

Nous avons pris soin d'importer le framework unittest de Python afin de disposer de la classe TestCase et du test runner. Nous avons également importé le webdriver fourni par Selenium, qui va nous permettre d'automatiser le navigateur. Les méthodes setUp() et tearDown() d'xUnit sont implémentées pour initialiser notre driver avant de lancer les tests et le fermer une fois terminé.

OK, nous sommes à présent fin prêts pour écrire notre code de test.

Astuce Il est possible d'exporter un script directement depuis Selenium IDE, via le menu « Fichier > Exporter le test sous… ».

tests.py
# -*- coding: UTF-8 -*-
import unittest
from selenium import webdriver

class TestCesiSearch(unittest.TestCase):

    def setUp(self):
        self.driver = webdriver.Firefox()

    def test_results_page_shows(self):
        self.driver.get("http://www.cesi.fr")
        select = Select(self.driver.find_element_by_id('form_1579'))
        select.select_by_value("Informatique et systèmes d'information")
        btn = self.driver.find_element_by_css_selector("input[type=\"submit\"]")
        btn.click()

        page_url = self.driver.current_url
        page_title = self.driver.find_element_by_css_selector(".txtblancRechercheTitre").text

        self.assertEqual(page_url, 'http://www.cesi.fr/recherche-formulaire-portail.asp')
        self.assertEqual(page_title, u'VOTRE RÉSULTAT, VOTRE RECHERCHE')

    def tearDown(self):
        self.driver.close()


if __name__ == '__main__':
    unittest.main()

Notre méthode de test test_results_page_shows() a un contenu relativement simple à comprendre : nous chargeons la page désirée, ciblons les éléments qui nous intéressent, cliquons où il faut, puis nous terminons par deux assertions pour vérifier que nous sommes atterris sur la bonne page, et que le titre est celui attendu.

Lançons à présent nos tests :

$ python test.py

.
----------------------------------------------------------------------
Ran 1 test in 21.666s

OK

OK, tout va bien, notre test passe ! Passons au second test, qui testera qu'une recherche sans critère abouti bien à l'affichage d'un message d'erreur.

tests.py
def test_no_criteria_implies_error_message(self):
    self.driver.get("http://www.cesi.fr")
    btn = self.driver.find_element_by_css_selector("input[type=\"submit\"]")
    btn.click()

    page_url = self.driver.current_url
    error_message = self.driver.find_element_by_css_selector(".listeDomainesFormation li").text

    self.assertEqual(page_url, 'http://www.cesi.fr/recherche-formulaire-portail.asp')
    self.assertEqual(error_message, u'Vous devez selectionner au moins 1 critère de recherche')

Relançons nos tests :

$ python test.py
F.
======================================================================
FAIL: test_no_criteria_implies_error_message (__main__.TestCesiSearch)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test.py", line 33, in test_no_criteria_implies_error_message
    self.assertEqual(error_message, u'Vous devez selectionner au moins 1 critère de recherche')
AssertionError: u'Vous devez selectionner au moins un crit\xe8re de recherche' != u'Vous devez selectionner au moins 1 crit\xe8re de recherche'
- Vous devez selectionner au moins un crit\xe8re de recherche
?                                  ^^
+ Vous devez selectionner au moins 1 crit\xe8re de recherche
?                                  ^


----------------------------------------------------------------------
Ran 2 tests in 19.767s

FAILED (failures=1)

Hum, le premier test est toujours OK, mais une erreur se produit sur le second test : le résultat obtenu n'est pas conforme au résultat attendu ! Deux solutions :

  • Modifier le code de l'application si c'est bien le résultat attendu qui doit être affiché
  • Modifier le code de test s'il s'agit une erreur de test (faux négatif)

En l’occurrence, il s'agit d'une erreur de test : modifions notre code de test :

self.assertEqual(error_message, u'Vous devez selectionner au moins un critère de recherche')

Relançons nos tests :

$ python test.py
..
----------------------------------------------------------------------
Ran 2 tests in 19.846s

OK

Nos deux tests ont bien été exécutés, et à présent tout le monde est OK, tout va bien…

Conclusion

Nous avons vu dans ce tutoriel comment mettre en œuvre des tests fonctionnels automatisés grâce à Selenium et son API webdriver, notamment en langage Python.

La grande force du testing fonctionnel automatisé couplé avec un framework de test technique (comme unittest en Python, PHPUnit en PHP, jUnit en Java…) est qu'il n'existe plus de frontière stricte entre tests fonctionnels et tests unitaires, tests d'intégration, etc.

Très important La distinction testing fonctionnel vs. testing unitaire/d'intégration n'a finalement pas de sens : les tests unitaires et d'intégration sont en réalité des tests fonctionnels, car tout est fonction dans un système informatique ! Selenium et ses API permettent de mettre en œuvre ce principe fondamental.

Testez vos connaissances

Quelle est la différence entre l'automatisation de navigateur (browser automation) et l'automatisation de tests ?
  • On peut automatiser des actions dans le navigateur dans réaliser de tests !
  • Il n'y a pas de différence, ces concepts sont équivalents.
  • L'automatisation de navigateur permet seulement d'éviter de réaliser des actions manuellement dans le navigateur, automatisation de tests va plus loins en confrontant les résultats observés à des résultats attendus.
L'industrialisation des tests passe-t-elle systématiquement par l'automatisation ?
  • Oui
  • Non

Industrialiser ne peut pas systématiquement dire automatiser : un process peut être industrialisé (formalisé, normé, documenté, contrôlé, etc.) en restant manuel (exemple d'une usine d'assemblage manuel de pièces détachées).

Quel est l'intérêt de l'outil Selenium IDE ?
  • Il permet de simplifier l'écriture des scripts Selenium.
  • Selenium est la seule manière d'écrire des scripts Selenium.
  • L'enregistrement de macros y est simplifié dans le navigateur.
  • Selenium IDE n'a aucun intérêt.
Dans quelle mesure le couplage entre Selenium et un framework de test comme unittest est intéressant ?
  • C'est la seule manière de pouvoir utiliser l'API Selenium sous Python.
  • Cela permet de mêler tests unitaires, tests d'intégration et tests d'interface.
  • Cela permet de disposer des assertions et de l'une implémentation de xUnit pour exécuter des tests Selenium.

Que permet de réaliser la commande Selenium suivante (API Python) ?

self.driver.find_element_by_id('find_element_by_id')
  • Cet énoncé est faux : on retrouve deux fois l'élément « find_element_by_id ».
  • Cette instruction permet de récupérer l'élément du DOM ayant pour identifiant find_element_by_id.
  • Cette instruction permet de récupérer l'élément du DOM ayant pour classe CSS find_element_by_id.

Livres sur Selenium

Selenium Webdriver in Java: Learn With Examples

Selenium Webdriver in Java: Learn With Examples

Voir

Selenium 1.0 Testing Tools: Beginner's Guide

Selenium 1.0 Testing Tools: Beginner's Guide

Voir