Vous avez vu les modules pour faire les tests, mais dès que vous allez vouloir faire des tests sérieux, vous allez vous heurter à la dure réalité.
La réalité est que pour tester, il vous faut la réalité.
Par exemple, si vous tapez dans une base de données, il vous faut une base de données opérationnelle. Pour tester un téléchargement, il vous faut une connexion internet. Pour tester si votre API fonctionne, il faut lancer un serveur Web.
Autre chose, si votre code appelle un autre code, comment vous assurer que cet appel a bien eu lieu ? Il faudrait aussi un logger pour tous les appels, et si le code n’est pas le vôtre, c’est encore plus chiant.
Comme nous savons que les informaticiens sont des grosses larves, il y a forcément une solution, au moins partielle, à ces problèmes. En l’occurrence, on va jouer au docteur, à la dinette, aux cowboys et aux indiens.
Bref, on va jouer à faire semblant.
Les objets mocks
Un objet mock, c’est un objet basé sur le null object pattern qui sert à faire semblant. Quand on l’instancie avec n’importe quoi, ça marche, quand on appelle n’importe quelle méthode, ça marche et ça renvoie un mock.
Bien entendu, comme les besoins des tests sont un peu plus raffinés que ça, mock fait plus que du null object pattern, et permet :
- De configurer son API.
- De configurer ses sides effects.
- De configurer sa valeur de retour.
- De monkey patcher un autre objet.
- D’enregistrer tous les appels qu’on lui fait.
Alors évidement, comme ça, je me doute bien que la puissance de l’outil ne vous frappe pas en face comme le nez au milieu de l’eureka dans un couloir.
C’est pour ça qu’on va passer aux exemples concrets. D’abord, assurez-vous de pouvoir faire import unittest.mock
, qui est dispo depuis Python 3.3. Si ce n’est pas le cas, l’installer avec pip install mock
vous permettra de l’importer sous la forme import mock
. Le reste, c’est pareil.
On dirait que moi je t’attaque et toi tu meurs pas
Un objet mock est un callable, c’est-à-dire qu’il peut être appelé comme une fonction ou une classe, et il retourne toujours un objet mock :
>>> from unittest.mock import MagicMock # ou from mock import MagicMock >>> mock = MagicMock() >>> print(mock) <MagicMock id='140302100559296'> >>> mock() <MagicMock name='mock()' id='140302101821704'> >>> mock(1, True, [Exception, {}]) <MagicMock name='mock()' id='140302101821704'> |
On peut appeler n’importe quoi sur son objet mock, et ça retourne toujours un objet mock :
>>> mock.foo() <MagicMock name='mock.foo()' id='140302101723960'> >>> mock.nimporte().nawak().je().te().dis() <MagicMock name='mock.nimporte().nawak().je().te().dis()' id='140302101825744'> >>> mock + mock - 10000 <MagicMock name='mock.__add__().__sub__()' id='140302134081520'> |
Quand retourner un objet mock n’est pas possible, l’objet essaye d’avoir le comportement qui fera planter le moins possible :
>>> int(mock) 1 >>> [m for m in mock] [] |
On dirait que le bâton, là, c’est un sabre laser
Parfois, néanmoins, il est utile de vouloir avoir un comportement spécifique. Il se trouve que les méthodes des objets mocks peuvent être des objets mocks. Mock, mock, mock !!!!!
Et les objets mocks peuvent être configurés pour avoir un effet de bord ou une valeur de retour :
mock.you = MagicMock(side_effect=ValueError('mofo !')) # un callable marche aussi >>> mock.you() Traceback (most recent call last): File "<ipython-input-21-a7e6455585e9>", line 1, in <module> mock.you() File "/usr/lib/python3.4/unittest/mock.py", line 885, in __call__ return _mock_self._mock_call(*args, **kwargs) File "/usr/lib/python3.4/unittest/mock.py", line 941, in _mock_call raise effect ValueError: mofo ! >>> mock.mock = MagicMock(return_value="moooooooooock") >>> mock.mock() 'moooooooooock' |
Cela vous permet d’utiliser les objets mocks comme des remplacements pour des objets réels dans vos tests mais chiants à instancier comme une event loop, un serveur, une connexion à une base de données… Ca permet aussi de remplacer des appels très longs par des trucs instantanés.
Mais la partie vraiment fun, c’est qu’on peut associer des vrais objets avec des objets mocks :
>>> class VraiObjetSerieuxEtTout: ... def faire_un_truc_super_serieux(self): ... return "... and don't call me Shirley" ... def faire_un_autre_truc_serieux(self): ... return "why so serious ?" ... >>> sirius = VraiObjetSerieuxEtTout() >>> sirius.faire_un_truc_super_serieux = MagicMock() # It's a kinda magic, magic ! >>> sirius.faire_un_autre_truc_serieux() 'why so serious ?' >>> sirius.faire_un_truc_super_serieux('ieux').delamort()[3:14] + [1, 2] <MagicMock name='mock().delamort().__getitem__().__add__()' id='140302103288296'> |
Et là ça devient super sympa : vous pouvez utilisez vos vrais objets, et pour certains appels, juste vous faciliter la vie pour les tests.
On dirait qu’on compte le nombre de balles que tu as tirées
Puisque les objets mocks sont un peu les grosses salopes de la programmation et acceptent tout ce qui vient (oups, je viens de tuer l’ambiance métaphore enfantine là), il peut être nécessaire de vérifier ce qui s’est passé. Or il se trouve qu’ils intègrent un historique des appels :
>>> sirius.faire_un_truc_super_serieux.mock_calls [call('ieux'), call().delamort(), call().delamort().__getitem__(slice(3, 14, None)), call().delamort().__getitem__().__add__([1, 2])] |
Et comme vérifier qu’un appel a bien eu lieu est une tâche courante, des méthodes pour les tests unitaires ont été intégrées :
>>> sirius.faire_un_truc_super_serieux.assert_called_with('ieux') >>> sirius.faire_un_truc_super_serieux.assert_called_with('not_ieux') Traceback (most recent call last): File "<ipython-input-56-e8c4890f08d9>", line 1, in <module> sirius.faire_un_truc_super_serieux.assert_called_with('not_ieux') File "/usr/lib/python3.4/unittest/mock.py", line 760, in assert_called_with raise AssertionError(_error_message()) from cause AssertionError: Expected call: mock('not_ieux') Actual call: mock('ieux') |
On dirait que tu vas mettre ces porte-jartelles et…
Pour finir, le module mock
vient avec patch()
, qui sert à, surprise, patcher les objets, et propose des context managers et des décorateurs pour se faciliter la vie.
Par exemple, détourner open()
temporairement :
>>> from unittest.mock import patch, mock_open >>> with patch('__main__.open', mock_open(read_data='wololo'), create=True) as mock: ... with open('zefile') as h: ... result = h.read() ... >>> mock.assert_called_once_with('zefile') >>> assert result == 'wololo' |
Ou alors avoir une partie d’un module qui soit un mock pour tout un appel de fonction :
@patch('os.listdir') def ah(mock): import os print(os.listdir('.')) # l'objet mock initial est aussi passé en param automatiquement print(mock) ah() ## <MagicMock name='listdir()' id='140302096346864'> ## <MagicMock name='listdir' id='140302101454688'> |
Le module mock est vraiment très complet, avec des outils pour checker les signatures, passer isinstance()
, overrider le contenu d’un dico, et tout un tas de cas particuliers et corner cases. Donc lisez la doc si vous rencontrez un blocage avant de paniquer.
Dis, comment on fait les bébés
Exemple prit d’une base de code IRL, avec une fonction pytest qui teste un objet response
représentant une réponse HTTP. Si on appelle write()
sur cet objet sous-jacent elle doit faire des appels à deux méthodes privées et une méthode d’un objet Twisted.
Problème, ces méthodes :
- Supposent qu’une event loop est lancée.
- Ecrivent sur le réseau.
- Sont potentiellement appelées de manière asynchrone, en dehors de notre contrôle.
- Ont des side effects donc on veut être certains qu’elles sont appelées, et avec les bons paramètres.
Du coup, on les remplace par des objets mocks, et yala :
def test_write(response): assert response.write != response._req.write response._disable_rendering = MagicMock(name='_disable_rendering') response._set_twisted_headers = MagicMock(name='_set_twisted_headers') response.write(b'test') response._set_twisted_headers.assert_called_once_with() response._disable_rendering.assert_called_once_with() assert response.write == response._req.write response._req.write.assert_called_once_with(b'test') |
Et pourquoi ? Et pourquoi ? Et pourquoi ?
Prochaines étapes, savoir quand tester, et quoi tester, mais aussi comment rendre un code plus testable. Probablement la partie qui sera la plus difficile à écrire pour moi, car c’est assez subjectif. On parlera sans doute du code coverage, et je gage que je vais devoir créer un petit projet bidon pour tester tout ça, du genre un minifieur d’URL ou autre. Faudra voir l’inspiration.
Merci pour cet article !
comment vous assurer que cet appel à bien eu lieu > comment vous assurer que cet appel a bien eu lieu
et si le code n’est pas le votre > et si le code n’est pas le vôtre
on, va jouer à faire semblant. > on va jouer à faire semblant.
On dirait que moi je t’attaques > On dirait que moi je t’attaque
Un objet mock est un callable, c’est à dire > Un objet mock est un callable, c’est-à-dire
Et là ça devient super sympas > Et là ça devient super sympa
On dirait qu’on compte le nombre de balles que tu as tiré > On dirait qu’on compte le nombre de balles que tu as tirées
Et comme vérifier qu’un appel à bien eu lieu est une tache > Et comme vérifier qu’un appel a bien eu lieu est une tâche
et des decorateurs pour se faciliter la vie. > et des décorateurs pour se faciliter la vie.
Dis, comment on fait les bébé > Dis, comment on fait les bébés
sur cet objet, sous jacent elle doit > sur cet objet sous-jacent, elle doit
Il l’avait dit, il l’a fait ! \o/
Bon, maintenant je vais lire…
Merci d’avance Sam !
Perso, les tests unitaires, ça m’a toujours gonflé et je trouve que ça sert à rien, mais je sais, je ne suis pas à la mode.
Histoire de faire mon chieur, pour pouvoir retrouver facile les differentes parties de ce guide, y’a moyen d’updater http://sametmax.com/aller-plus-loin-en-python/ qui en est resté a la partie 1?
Oui, mais pas avant juin, comme tu vas le comprendre avec l’article d’aujourd’hui.
<troll> Pourquoi on peut pas cliquer sur grosse salope </troll>
Super article merci !
C’est une erreur impardonnable qu’il nous faut corriger.
ca parait sympa j’vais jeter un coup d’oeil pour mes TU.
Cela dit je suis en général assez méfiant sur l’usage des mocks dans les TU. Notamment je pense que vérifier l’usage qui a été fait du mock par la classe qu’on est en train de tester (ex compter le nombre d’appels de telle méthode du mock, vérifier les paramètres d’appels…) est un peu foireux, parce que cela revient à vérifier l’implémentation interne de l’objet qu’on teste, et non son comportement externe-API. Si l’algo interne change, les TU tombent, alors que son comportement externe est peut-etre toujours valide…
Le projet sur lequel je suis en ce moment est plein de ces TU foireux, où finalement on ne vérifie que l’algorithme, pas le comportement externe – bon faut dire que l’API des objets qu’on teste est totalement merdique
Tout ça pour conclure : pas de TU de qualité possibles, sans une API béton
C’est vrai, simplement généralement les mocks vont justement être utilisé quand on va vouloir tester des choses qui n’ont pas justement une API bêton. Si un API est bien faite, on a finalement très peu besoin de mock, car tout est testable et indépendant.
Juste une remarque :
il manque un
from unittest.mock import mock_open
dans l’exemple sur les porte-jartelles :)Sinon super série d’articles, merci :)
Merci :)
Merci mille fois pour cette série d’articles : excellents, comme toujours !
Peut-on espérer en voir la fin un jour ? Keep it up!