mardi 24 mars 2020

Serveur ESP32 : tests automatisés

Serveur ESP32 : tests automatisés


Ceci est la suite des cinq articles précédents :
Serveur ESP32 : implémentation
Serveur ESP32 : implémentation (2ème partie)
Serveur ESP32 : implementation (3eme-partie)
Serveur ESP32 : implementation (4eme-partie)
Serveur ESP32 : implementation (5eme-partie)

Maintenant que notre serveur est bien avancé il serait temps de démarrer une séance de test. Nous allons pour cela utiliser PYTHON et le framework UNITTEST.

Ce serveur est l'occasion de démontrer l'intérêt des tests automatisés. Il sera facile d'envoyer des requêtes à celui-ci et de vérifier que tout se passe comme prévu.

1. Tests automatisés

Pourquoi des tests automatisés ?
J'ai l'habitude de travailler comme cela depuis 20 ans et ces tests comportent tellement d'avantages que je ne pourrais plus travailler autrement.

1.1. Gain de temps

Une fois que les tests sont écrits, le temps d'exécution est ridicule par rapport à celui que prendrait l'exécution des mêmes tests faits à la main.

1.2. Répétabilité

Les tests logiciels sont répétables à volonté. On peut les exécuter autant de fois que nécessaire.

1.3. Régressions

A chaque modification de code une régression est possible. Une évolution peut introduire un bug, y compris dans du code déjà existant, par effet de bord par exemple.
L'exécution des tests automatisés permet de s'assurer que le code fonctionne comme avant la modification.

Il est beaucoup plus facile de modifier du code en profondeur (refactoring) quand on dispose de tests déjà écrits. Une fois le code modifié, le déroulement des tests permet de s'assurer que le code fonctionne toujours.
Un refactoring permet de réécrire des portions de code mal conçues, et le fait de redevoir refaire les tests à la main est souvent dissuasif, donc le refactoring n'est pas effectué, ce qui est une erreur.

1.4. Tests finaux

Le déroulement des tests automatisés ne dispense pas de réaliser des tests finaux (intégration, validation).
Ces tests finaux permettront aussi de constater des bugs. Souvent un bug est dû à l'absence d'un test unitaire.
Idéalement, chaque bug doit être l'occasion d'écrire un nouveau test, et ce test doit être OK après correction du bug.

1.5. Sérénité

Lorsqu'un projet est terminé et que l'on doit installer une version terrain, le déroulement des tests automatisés permet de livrer son travail avec beaucoup plus d'assurance et moins d'appréhension.

2. Logiciels nécessaires

Pour réaliser nos tests nous allons avoir besoin de quelques logiciels sur PC.

Il existe plusieurs solutions très élaborées, dont SELENIUM, qui nécessitent une mise en œuvre plus lourde.

SELENIUM procède par pilotage en direct du navigateur à l'aide d'un WebDriver approprié (Firefox, Safari, Edge, Chrome, Internet Explorer, etc.).
Le navigateur est donc actif et on peut voir en direct la navigation opérer pendant le test, comme une vidéo :
  • clicks sur les boutons, liens
  • entrée de caractères au clavier
  • changements de page HTML
  • etc.
La technique que je propose permet d'utiliser des composants simples, à la portée de tout un chacun. Elle est assez facile à comprendre par toute personne, même peu habituée au développement WEB :
  • envoi d'une requête à un serveur WEB
  • réception de la réponse
  • examen de la réponse (parsing)
  • comparaison du résultat par rapport à un résultat attendu
  • affichage des éventuels problèmes
SELENIUM est un sujet à part entière, plus complexe, qui fera sans doute l'objet d'un autre article.

2.1 PYTHON

PYTHON est un langage interprété très moderne dont j'ai déjà parlé sur ce blog.

2.1.1. PYTHON 2
La version 2.7 commence à dater, mais elle est toujours maintenue. La dernière release date d'octobre 2019.
PYTHON 2.7 a été très utilisé par le passé, d'où un support encore très actif.

2.1.2. PYTHON 3
PYTHON 3 est sorti en 2008, et comme il s'agit ici d'un nouveau projet, nous allons plutôt opter pour ce choix.

La reprise d'un ancien projet PYTHON 2.7 pour le porter en PYTHON 3 nécessiterait pas mal de travail, en fonction de la taille du projet.

Pour installer PYTHON3 sous Ubuntu :

sudo apt-get install python3

Pour installer PYTHON3 sous Windows :

https://www.python.org/downloads/

Choisir la dernière version (3.7.7 actuellement) :

https://www.python.org/downloads/release/python-377/

En bas de page choisir "Windows x86 executable installer" ou "Windows x86-64 executable installer", en fonction de l'architecture 32bits ou 64bits du PC.

2.2. UNITTEST

UNITTEST est un framework de tests unitaires PYTHON.
Il permet d'exécuter des tests automatisés. Les tests peuvent être organisés :
  • module (un programme PYTHON)
    • suite de test (une classe)
      • test (une méthode de la classe)
# test.py
import unittest

class TestStringMethods(unittest.TestCase):

    def test_upper(self):
        self.assertEqual('foo'.upper(), 'FOO')

    def test_isupper(self):
        self.assertTrue('FOO'.isupper())
        self.assertFalse('Foo'.isupper())

    def test_split(self):
        s = 'hello world'
        self.assertEqual(s.split(), ['hello', 'world'])
        # check that s.split fails when the separator is not a string
        with self.assertRaises(TypeError):
            s.split(2)

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


Il est possible d'exécuter tous les tests d'un même module, une seule classe ou un test en particulier :

python3 test.py
python3 test.py TestStringMethods
python3 test.py TestStringMethods.test_upper

2.3. URLLIB et HTMLParser

URLLIB est une librairie permettant d'envoyer une requête à un serveur et d'attendre sa réponse :

from urllib import request, parse
 

# envoi d'une requête GET
html = request.urlopen('http://192.168.1.18').read()
 

# envoi d'une requête POST
data = parse.urlencode({'action': 'add1'}).encode("ascii")
html = request.urlopen('http://192.168.1.18/', data=data).read()


URLLIB fait partie des librairies de base de PYTHON.

HTMLParser va nous permettre de décortiquer la réponse du serveur :

from html.parser import HTMLParser
 

class MyHTMLParser(HTMLParser):

  def __init__(self):
        HTMLParser.__init__(self)

        self.state = 'idle'
        self.title = ''

    def handle_starttag(self, tag, attrs):

        print("Encountered a start tag:", tag, attrs)
        if (tag == 'title') :
            self.state = 'title'

    def handle_endtag(self, tag):
        print("Encountered an end tag :", tag)
        self.state = 'idle'

    def handle_data(self, data):
        print("Encountered some data  :", data)

        if self.state == 'title':
            self.title = data


parser = MyHTMLParser()
parser.feed(str(html))

Les méthodes de la classe MyHTMLParser doivent être complétées pour récupérer les informations qui nous intéressent :
  • textes de label, de bouton
  • contenu et couleur d'un champ de saisie
Les lignes print() peuvent être laissées dans un premier temps pour y voir plus clair, et retirées ensuite.

L'exemple fourni est complet : voir 8. Téléchargement
Il s'agit d'un petit automate à états finis très simple à comprendre.

HTMLParser fait également partie des librairies de base de PYTHON.

Avec UNITTEST + URLLIB et HTMLParser nous avons donc tout le matériel nécessaire pour élaborer nos tests.

5. Exemple

    def test_first_launch(self):
        html = request.urlopen('http://192.168.1.18').read()
        self.parser.feed(str(html))
        self.assertEqual(self.parser.title, 'Subscribers')
        self.assertEqual(self.parser.page_title, 'SUBSCRIBERS')
        self.assertEqual(self.parser.subscriber_name, 'Unknown')
        self.assertEqual(self.parser.subscriber_id, '0')
        self.assertEqual(self.parser.subscriber_credit, '0 credits')
        self.assertEqual(self.parser.subscriber_history, '\\n')
        self.assertEqual(self.parser.status, 'No Subscribers registered')
        self.assertEqual(self.parser.status_color, 'background-color:red;color:white;')


Ici nous testons le premier lancement de notre serveur :
  • la requête est envoyée et la réponse reçue
  • les informations dont vérifiées :
    • le titre de la page : "Subscribers"
    • un titre en gras : "SUBSCRIBERS"
    • le nom de l'abonné "Unknown"
    • l'ID de l'abonné "0"
    • le solde de l'abonné "0 credits"
    • l'historique : vide
    • le status : "No Subscribers registered"
    • la couleur du status : blanc sur fond vert
Exécution :

$ python3 test.py Test.test_first_launch
.
----------------------------------------------------------------------
Ran 1 test in 0.662s

OK


L'exécution a réussi.

5. Quelques règles :

UNITTEST considère qu'une méthode est une méthode de test si son nom commence par "test". Les autres méthodes sont ignorées. Bien entendu les méthodes de test peuvent faire appel à toute méthode classique, ne sesait-ce que pour des raisons de factorisation.

Deux méthodes setUp() et tearDown() peuvent être appelées automatiquement par UNITTEST, si elles existent, en début et fin de chaque test :
  • setUp() : initialisations communes à tous les tests de la classe
  • tearDown() : terminaisons communes à tous les tests de la classe
La méthode spéciale setUp() est exécutée avant chaque test :

    def setUp(self):
        self.parser = MyHTMLParser()


Elle nous sert simplement à créer notre objet MyHTMLParser.

L'autre méthode spéciale tearDown() est exécutée après chaque test :

    def clear_all(self):
        request.urlopen('http://192.168.1.18/wipe.html').read()

   def tearDown(self):
        self.clear_all()


Elle est utilisée pour nettoyer le serveur après chaque test.

Pourquoi nettoyer après chaque test ? parce que UNITTEST lance les tests les uns à la suite des autres sans ordre précis.

Un test ne doit pas dépendre des conditions finales du test qui a été exécuté précédemment.

Par exemple le test de l'exemple du paragraphe 4 échouerait s'il était exécuté après un autre test ayant créé un abonné.
Le status en particulier ne serait pas ègal à "No Subscribers registered", forcément.

6. La testabilité

Le serveur ESP32 a été développé dès le départ pour être logiciellement testable.

Par exemple les champs d'un formulaire ont un nom explicite qui permet de les retrouver facilement dans la réponse à une requête.

Quelques requêtes supplémentaires ont été ajoutées :
  • visialisation des fichier sur le serveur
  • destruction des fichier sur le serveur
Dans cette version j'ai ajouté une requête permettent l'ajout d'un abonné en fournissant son nom et son ID. Cette méthode permet de se passer de la présentation de la carte RFID.

S'il fallait présenter une carte pour exécuter certains tests, cela ne s'appellerait plus du test automatisé.

Mais cela n'empêche pas d'écrire une suite de test spéciale dite "suite interactive" avec une interaction homme machine.

Exemple :
  • le test demande la présentation d'une carte
  • le testeur présente la carte et appuie sur <RETURN>
  • le test continue
Cette suite interactive sera déroulée suivant les besoins, probablement moins souvent que la suite de tests automatisés.

7. La suite de test

Celle-ci est disponible ici :
https://bitbucket.org/henri_bachetti/webserver-form/src/v2.2/esp32-subscriber/test/test.py

Elle comporte 3 tests :
  • test de premier lancement du serveur
  • test d'ajout d'un abonné
  • test d'ajout de 1 crédit + 5 crédits
Exécution :

$ python3 test.py
...
----------------------------------------------------------------------
Ran 3 tests in 2.003s

OK


L'exécution prend 2 secondes. Ce temps est bien inférieur à celui qu'il aurait fallu pour faire la même chose manuellement avec la souris et le clavier.

8. Téléchargement

Cette version 2.2 est disponible ici :
https://bitbucket.org/henri_bachetti/webserver-form/src/v2.2/esp32-subscriber/

9. Liens utiles

La suite :
Serveur ESP32 : tests automatisés (2ème partie)
Serveur ESP32 : implémentation (6eme-partie)


10. Conclusion

Dans le cadre d'un développement d'un serveur ESP32 cette méthode de test est facile à mettre en place, car le canal de communication permettant de dérouler les tests existe déjà : HTTP.

Faire la même chose avec un ARDUINO par la ligne série n'est pas impossible. On peut s'inspirer de cet article :
https://riton-duino.blogspot.com/2019/12/commander-un-arduino-par-la-ligne-serie_21.html

Cette suite de tests m'a déjà permis de trouver un bug dans gestion de la requête de nettoyage. La fiabilité ne peut qu'augmenter en continuant.

Bien sûr les tests sont loin d'avoir été tous écrits. Par exemple le contenu des fichiers n'est pas vérifié. Le travail continue donc.

J'espère vous avoir éveillé votre curiosité avec cette démarche de test.


Cordialement
Henri

Aucun commentaire:

Publier un commentaire