Il sera question de tests unitaires en Python.
On ne parlera pas de méthodes de développement ou de gestion de projet. Méthodes agiles, Extreme Programming, Test Driven Development (TDD) ce n'est pas le sujet, mais vous avez les mots clés vous pouvez toujours vous informer.
Le web est plein de beaux discours sur l'utilité des tests. Dans notre cas on se limitera à dire que les tests peuvent sauver des vies.
L'intérêt principal des tests est de s'assurer qu'en modifiant un code existant on ne casse pas ce qui fonctionnait avant ou pire que les bugs qu'on avait résolus ne reviennent pas. On appelle ça des tests de non-régression. C'est très utile.
Il nous faut des exemples. Pour cela on va s'appuyer sur le code de la classe Word
et de ses classes filles.
import re
class Word:
""" Classe Word : définit un mot simple de la langue """
type = "simple"
def __init__(self, *args):
self.form, self.lemma, self.pos = args
def is_inflected(self):
if self.form != self.lemma:
return True
else:
return False
def magic_compare(self, other_word):
diff = []
for key in self.__dict__.keys():
if self.__dict__[key] != other_word.__dict__[key]:
diff.append(key)
return diff
def __str__(self):
return " ".join((self.form, self.lemma, self.pos))
class WordTreeTagger(Word):
def __init__(self, formatted_str):
"""
import a formatted_str in treetagger format
"""
pattern = re.compile("(\w+)\t(\w+)\t(\w+)")
res = pattern.search(formatted_str)
self.form, self.lemma, self.pos = res.groups()
class WordBrown(Word):
def __init__(self, formatted_str):
"""
import a formatted_str in brown format
"""
pattern = re.compile("(\w+)/(\w+)/(\w+)")
res = pattern.search(formatted_str)
self.form, self.lemma, self.pos = res.groups()
class WordSem(Word):
def __init__(self, formatted_str):
"""
import a formatted_str in sem format (brown with no lemmas)
"""
pattern = re.compile("(\w+)/(\w+)")
res = pattern.search(formatted_str)
self.form, self.pos = res.groups()
self.lemma = ""
assert
sert à ça¶Normalement on met les instructions de tests dans un fichier distinct.
L'idée ici est de tester toutes les valeurs possibles que l'on pourra recontrer pour tester la robustesse du code (oui comme dans CodinGame). On commence par une valeur standard, un truc facile puis on va chercher les trucs vicieux.
Dit autrement le but est de faire planter le code pour pouvoir l'améliorer.
obj_sem = WordSem("tests/NC")
assert obj_sem.lemma == ""
Il ne se passe rien, c'est que tout va bien.
obj_sem = WordSem(";/PONCT")
assert obj_sem.form == ";"
obj_sem_2 = WordSem("aujourd'hui/NC")
assert obj_sem_2.form == "aujourd'hui"
obj_sem_3 = WordSem("13/12/2017/NC")
assert obj_sem_3.form == "13/12/2017"
Blam ! assert
lève une exception de type AssertionError
quand la condition testée n'est pas remplie. Le programme est interrompu.
Le problème ici est que seule la première instruction assert
est testée, les deux restantes ne sont pas examinées.
unittest
les tests canal historique¶unittest
est inspiré de Junit
le framework de test qui a rendu les tests populaires, enfin non il les a popularisés.
Pour utiliser unittest
il faut écrire une classe dont le nom commence par Test. La classedevra aussi hériter de unittest.TestCase
.
Recopier le code suivant dans un fichier test_word_sem.py
(les fichiers de test commencent systématiquement par test_
.
import unittest from word import WordSem
class TestWordSem(unittest.TestCase): def test_init_simple(self): obj_sem = WordSem("été/NC") self.assertEqual(obj_sem.form, ";")
def test_init_ponct(self):
obj_sem = WordSem(";/PONCT")
self.assertEqual(obj_sem.form, ";")
def test_init_apostrophe(self):
obj_sem = WordSem("aujourd'hui/NC")
self.assertEqual(obj_sem.form, "aujourd'hui")
def test_init_slash(self):
obj_sem = WordSem("13/12/2017/NC")
self.assertEqual(obj_sem.form, "13/12/2017")
if name == 'main': unittest.main()
%run test_word_sem.py -v
# python3 test_word_sem.py -v sur votre console shell
1 erreur, 3 échecs et un succès, il reste du travail. Vous noterez que l'ouput est suffisamment détaillé pour comprendre exactement où se situent les erreurs.
Je vous invite à lire la doc de unittest
pour en savoir plus sur :
assertQuelqueChose
du modulessetUp
qui permet d'initialiser la classe de test avec les données nécessaires aux tests.pytest
¶unittest
c'est bien mais un peu fastidieux à mettre en oeuvre quand même.
Il existe plusieurs environnements tests en Python mais la meilleure alternative à unittest
c'est pytest
.
Avec pytest
voici de que devient notre fichier de test :
from word import WordSem
def test_init_simple():
obj_sem = WordSem("été/NC")
assert obj_sem.form == "été"
def test_init_ponct():
obj_sem = WordSem(";/PONCT")
assert obj_sem.form == ";"
def test_init_apostrophe():
obj_sem = WordSem("aujourd'hui/NC")
assert obj_sem.form == "aujourd'hui"
def test_init_slash(self):
obj_sem = WordSem("13/12/2017/NC")
assert obj_sem.form == "13/12/2017"
C'est tout de suite plus attrayant.
Pour lancer les tests tapez la commande pytest
. C'est tout ? Oui c'est tout.
!pytest
doctest
¶Le module doctest
permet d'insérer des tests dans les docstrings. Les tests ne seront pas dans un ou des fichiers distincts mais directement dans le code.
Les tests sont écrits à la manière d'une session python dans le shell.
Vous noterez l'appel au module à la fin du fichier
class WordTreeTagger(Word):
def __init__(self, formatted_str):
"""
import a formatted_str in treetagger format
"""
pattern = re.compile("(\w+)\t(\w+)\t(\w+)")
res = pattern.search(formatted_str)
self.form, self.lemma, self.pos = res.groups()
class WordBrown(Word):
def __init__(self, formatted_str):
"""
import a formatted_str in brown format
"""
pattern = re.compile("(\w+)/(\w+)/(\w+)")
res = pattern.search(formatted_str)
self.form, self.lemma, self.pos = res.groups()
class WordSem(Word):
def __init__(self, formatted_str):
"""
import a formatted_str in sem format (brown with no lemmas)
>>> obj_sem = WordSem("été/NC")
>>> obj_sem.form
'été'
"""
pattern = re.compile("(\w+)/(\w+)")
res = pattern.search(formatted_str)
self.form, self.pos = res.groups()
self.lemma = ""
if __name__ == "__main__":
import doctest
doctest.testmod()
On appelle les tests avec la commande python3 monficher.py
tout simplement, il ne se passe rien si les tests sont validés. Plus de détails avec l'option -v
.
class WordSem(Word):
def __init__(self, formatted_str):
"""
import a formatted_str in sem format (brown with no lemmas)
>>> obj_sem = WordSem("été/NC")
>>> obj_sem.form
'été'
>>> obj_sem = WordSem(";/PONCT")
>>> obj_sem.form
';'
>>> obj_sem = WordSem("aujourd'hui/NC")
>>> obj_sem.form
"aujourd'hui"
>>> obj_sem = WordSem("13/12/2017/NC")
>>> obj_sem.form
'13/12/2017'
"""
pattern = re.compile("(\w+)/(\w+)")
res = pattern.search(formatted_str)
self.form, self.pos = res.groups()
self.lemma = ""
if __name__ == "__main__":
import doctest
doctest.testmod()
Pour des tests simples, comme les nôtres ici, doctest
est parfait. C'est léger, pas de fichier distinct et en plus les tests permettent de documenter le code et son utilisation.
doctest
ne convient pas par contre aux tests qui nécessitent une initialisation lourde (connexion à une base de données par ex.). On ne trouve pas d'équivalent de la méthode setUp
de unittest
ou des fixtures de pytest
dont on n'a pas parlé.
Rien ne vous empêche d'utiliser doctest
et pytest
par exemple.
Il ne nous reste plus qu'à corriger le code pour passer les tests maintenant.