
Ouais c'est long et rébarbatif ...
C'est du temps de perdu
Je le ferai quand j'aurais du temps , là il faut que je délivre le produit
J'ai juste changé une ligne
Ce bout de code vole déjà
Mieux gérer les risques : détecter les bugs avant la sortie en production, améliorer le confort des équipes, avoir un filet de sécurité
Étendre la responsabilité individuelle : s'assurer du niveau de qualité d'un logiciel avant de le mettre à disposition
Avoir un retour rapide sur votre développement
Se prémunir des cas d’utilisation invalides
Eviter d'accumuler de la dette technique avec une application maintenable et évolutive
C'est un retour sur investissement !
L'écriture de tests peut prendre du temps donc cette étape doit être prévue dans le cycle de développement
Plutôt que prévoir un temps dévolu seulement à l'écriture des tests, faite le au fur et à mesure
Ou même avant de coder - cf TDD
Adaptez votre code pour qu'il soit testable, n'adaptez pas vos tests ! Votre code va gagner en lisibilité et découplage
Il ne communique pas avec la base de données
Il ne communique pas avec d’autres ressources sur le réseau
Il ne manipule pas un ou plusieurs fichiers
Il peut s’exécuter en même temps que les autres tests unitaires
Il est automatique
Il est répétable
100% des appels externes du composant sont simulés par des bouchons.
Un test doit être simple, normé, commenté, facile à lire et à comprendre.
Un test doit être rapide à exécuter (la suite de tests doit pouvoir être exécutée souvent).
Les tests doivent être écrits le plus tôt possible (il n’y a jamais de « plus tard »).
Un test = une assertion.
Les parties critiques de l’application sont testées en priorité.
Le code de l’application doit être écrit et refactorisé de façon à être testable.
Le nom d’un test doit être normalisé (la lecture du nom doit permettre de savoir exactement ce que va faire le test).
« Arrange » : initialisation du contexte d’exécution du test unitaire (variables, données, dépendances, bouchons…)
« Act » : exécution du traitement nécessaire pour la vérification du comportement du code testé
« Assert » : vérification du critère de réussite du test au travers d’une assertion.
les tests de cas normaux, qui vérifient que l'unité se comporte bien dans les situations « normales »
les tests de cas d'erreur, qui vérifient que les erreurs qui doivent être signalées le sont bien, p.ex. lorsqu'un argument invalide est fourni
les tests de cas aux limites, qui vérifient que l'unité se comporte bien dans les situations délicates, p.ex. qu'une méthode qui accepte un tableau de taille quelconque fonctionne correctement s'il est vide
pour des arguments entiers : tester avec 0 ainsi que des entiers positifs et négatifs
pour les chaînes de caractères : tester avec une chaîne vide, une chaîne contenant des caractères quelconques, une chaîne contenant des mots séparés par des espaces, virgules ou des retours à la ligne, voir si les chiffres ou caractères spéciaux sont bien supportés, voir s’il est possible de traiter une très longue chaîne de caractères, ...
pour une liste : tester avec une liste vide, une liste avec un seul élément, une liste contenant des éléments tous différents, une liste contenant plusieurs fois le même élément, …
pour des fichiers : tester avec des fichiers vides, valides et invalides
Python comprend un module de testing assez complet et pratique nommé unittest.
unittest permet de définir des cas de tests, avec notamment un jeu d'assertions assez complet et
pratique.
Alternatives possibles : Doctest , py.test, Hypothesis, tox, mock
unittest permet
l'automatisation des tests
le partage de code pour la mise en place et la finalisation des tests
l'agrégation de tests en collections
Le module unittest est utilisable depuis la ligne de commande pour exécuter des tests à partir de modules, de classes ou même de méthodes de test individuelles
python -m unittest test_module1 test_module2
python -m unittest test_module.TestClass
python -m unittest test_module.TestClass.test_method
Unittest prend aussi en charge une découverte simple des tests
Tous les fichiers de test doivent être des modules ou des paquets importables du répertoire du projet
cd project_directory
python -m unittest discover
Si vous avez installé coverage.py (voir présentation Outils d’analyse et de mesure de la qualité du code)
cd project_directory
coverage run -m unittest discover
Un scénario de test est une classe avec une unité logique groupant un certain nombre de tests
Dans unittest, les scénarios de test sont représentés par des instances de la classe unittest.TestCase
Pour créer vos propres scénarios de test, vous devez écrire des sous-classes de TestCase
Un test est une méthode dont le nom est préfixé par test_
Le cœur de chaque test est un appel aux méthodes assert*()
Si le test échoue, une exception est levée avec un message explicatif, et unittest identifie ce scénario de test comme un échec. Toute autre exception est traitée comme une erreur
import unittest
def is_even(nbr):
"""
Cette fonction teste si un nombre est pair.
"""
return nbr % 2 == 0
class MyTest(unittest.TestCase):
def test_is_even(self):
self.assertTrue(is_even(2))
self.assertFalse(is_even(1))
self.assertEqual(is_even(0), True)
if __name__ == '__main__':
unittest.main()
.
--------------------
Ran 1 test in 0.001s
OK
| Méthode | Test python équivalent |
|---|---|
assertEqual(a, b) |
a == b |
assertNotEqual(a, b) |
a != b |
assertTrue(x) |
bool(x) == True |
assertFalse(x) |
bool(x) == False |
assertIs(a, b) |
a is b |
assertIsNot(a,b) |
a is not b |
assertIsInstance(a, b) |
isinstance(a, b) |
|
Notons aussi l'existence de la méthode assertRaises qui vous permettra de vous assurer qu'une méthode lève bien le bon type d'exception en cas de problème |
|
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()
Sortie avec l'option -v
test_isupper (__main__.TestStringMethods) ... ok test_split (__main__.TestStringMethods) ... ok test_upper (__main__.TestStringMethods) ... ok ---------------------------------------------------------------------- Ran 3 tests in 0.001s OK
Le code de mise en place des tests peut être factorisé avec la méthode setUp()
Si la méthode setUp() lève une exception, le sytème considère le test en erreur
De même, on peut fournir une méthode tearDown() qui nettoie après l'exécution de la méthode de test
Si setUp() a réussi, tearDown() est exécutée, que la méthode de test ait réussi ou non.
Les décorateurs @unittest.skip("reason") and @unittest.skipIf(condition) peuvent être utilisés pour éviter un test.
import unittest
class WidgetTestCase(unittest.TestCase):
def setUp(self):
self.widget = Widget('The widget')
def test_default_widget_size(self):
self.assertEqual(self.widget.size(), (50,50), 'incorrect default size')
def test_widget_resize(self):
self.widget.resize(100,150)
self.assertEqual(self.widget.size(), (100,150), 'wrong size after resize')
def tearDown(self):
self.widget.dispose()
Cette librairie va permettre de simuler des classes, méthodes ...
Vous pouvez ainsi à partir des objets Mock et MagicMock créer tous les attributs et méthodes nécessaires à une simulation
class MyClass(object):
def __init__(self):
self.data = None
def readData(self, source):
self.data = source.read()
source.close()
import unittest
from mock import Mock
from mymodule import MyClass
def TestMyClass(unittest.TestCase):
def testConstructor(self):
"Test the default state"
myclass = MyClass()
self.assertEqual(myclass.data, None)
def testReadData(self):
#### Simulation de l'appel à une ressources
myclass = MyClass()
source = Mock()
source.read.return_value = 'some data'
myclass.readData(source)
self.assertEqual(myclass.data, 'some data')
self.assertTrue(source.read.called)
self.assertTrue(source.close.called)
import datetime
start_time = datetime.datetime.now()
# insert code snippet here
end_time = datetime.datetime.now()
elapsed = end_time - start_time
print(f'Temps d\'exécution : {elapsed:.2}ms')
Temps d'exécution : 0.04ms
import cProfile
import numpy as np
cProfile.run("20+10")
3 function calls in 0.000 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.000 0.000 <string>:1(<module>)
1 0.000 0.000 0.000 0.000 {built-in method builtins.exec}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}