PROJET AUTOBLOG


Sam & Max: Python, Django, Git et du cul

Site original : Sam & Max: Python, Django, Git et du cul

⇐ retour index

Un gros guide bien gras sur les tests unitaires en Python, partie 5 7

dimanche 17 mai 2015 à 09:35

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 :

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
>>> 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 :

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.

Pourquoi il est difficile de “juste ajouter un nouveau mot clé” 11

samedi 16 mai 2015 à 09:52

Avec les types hints, beaucoup ont suggéré l’ajout de nouveaux mots clés pour déclarer les types des paramètres et des variables. Guido a refusé, au moins pour les paramètres des fonctions, et s’en tient aux annotations.

Mais pourquoi être si têtu ? Après tout, c’est forward compatible, n’est-ce pas ?

Faux.

Quand on rajoute un mot clé, on ne peut plus l’utiliser comme nom de variable, et donc tout code qui utilise une variable qui porte ce nom va planter quand il passera à cette version de Python.

Inutile de dire qu’un mot clé doit être court et explicite, et que tous les mots courts et explicites sont utilisés comme noms de variables quelque part.

C’est pour cette raison que l’ajout de async/await a lancé toute une discussion sur la manière de changer la grammaire.

Et vous savez ce qui a été décidé ?

De ne PAS interdire de faire :

async = await = True
async def foo():
    await bar
# pas de SyntaxError

En clair, le parseur va détecter des cas particuliers pour ces instructions dans lesquelles il les identifie comme mot clés, et autoriser quand même leur usage comme variable dans tous les autres cas.

C’est un compromis pour garder la compatibilité ascendante, mais un compromis qui non seulement introduit un cas particulier dans le code du parseur (beurk), mais en plus permet d’écrire un code qui avant n’était pas permis, rendant le langage un peu moins cohérent. Un changement que maintenant on va se trainer pour les releases à venir, qui ouvre un précédent pour les futures débats, qui peut potentiellement s’accumuler avec d’autres changement futurs…

Faire évoluer un langage qui a 20 ans et dont il y a des millions de lignes de code partout, c’est dur. Chaque petite modification peut entrainer des conséquences importantes, et tout juste milieu renferme des craquelures d’inélégance.

C’est aussi pour ça qu’il est si plaisant d’inventer son langage, son framework, son outil pour faire x mieux que le status quo y. Il est parfait. Il est tout frais. Personne ne l’utilise. Il n’a encore eu aucune cicatrice.

A l’inverse, garder le cap malgré les remous, maintenir une diète saine et un équilibre mais accepter les saloperies qui passent de temps en temps, ça c’est difficile. Je suis épaté que Python soit resté si propre avec tout ce qui lui est arrivé au fil des années, et c’est grâce Guido qui a su dire NON à tout un tas de petits détails qui ne paraissaient pas importants, mais qui cumulés sur 2 décennies auraient transformé le langage en créature fantasque.

Pourvu qu’ça dure.

Un ptit post pour remercier tous les contributeurs 32

vendredi 15 mai 2015 à 21:50

Après ces quelques années de blogging je voulais remercier, et je pense pouvoir parler aussi au nom de Sam, tous ceux qui ont participé à la joie et la bonne humeur de ce blog.

Mais aussi à l’évolution d’IndexError où nous pouvons voir chaque jour avec un très grand plaisir des contributeurs aider sans critiquer ou juger, apporter des réponses ou donner des pistes sans rien demander en échange.

Merci à vous tous de donner de votre (précieux) temps pour aider votre “prochain” programmeur. La vitesse des réponses sur IndexError est tout juste hallucinante malgré la “petite” communauté.

De plus les commentaires sur le Blog sont toujours courtois et intéressants.

Je remercie en particulier pour IndexError, pour le Blog ça serait trop long à lister (mais j’en oublie certainement):

Foxmask, boblinux, jc, cOda, doublenain, furankun, Hawke et bien d’autres…

Aller je vais aux putes, ce post m’a épuisé.

Passez un bon Noël!

Le don du mois : Vasalgel 2

jeudi 14 mai 2015 à 09:56

Il n’y a pas de date pour le don du mois, ça part quand ça me prend.

Aujourd’hui l’inspiration s’est présentée sous la forme d’une newsletter que je reçois régulièrement à propos de couilles de babouins. Si, si.

Souvenez-vous, je suis l’avancée du projet RISUG, un contraceptif masculin semi-permanent et non hormonal.

Le concept est indien, mais la Parmesus Foundation essaye d’importer le procédé aux USA. Ca veut dire une batterie de tests à faire, l’approbation auprès des autorités, etc.

La fondation se présente avec ce but :

Advance innovative medical research and develop low-cost solutions that are neglected by the pharmaceutical industry.

C’est peut être du bullshit (“écrivez à l’arc” :)), et franchement difficile de m’assurer que mon argent ne finira pas dans les poches d’un business angel peu scrupuleux.

Mais personnellement, je suis prêt à payer très cher pour éviter toute forme de contraception contraignante tout en ayant la certitude de ne pas avoir d’enfants.

Du coup, je soutiens l’initiative, qui par ailleurs accepte les bitcoins. 0.25BTC partent donc dans leur poche, et je croise les doigts. Un jour peut être, je pourrai faire un saut à JFK pour faire brider mes nageurs.

Python 3 est fait pour les nouveaux venus 19

mardi 12 mai 2015 à 11:41

“Je n’ai pas vraiment de raison de migrer vers Python 3″ est la cause numéro 1 de la lente migration de Python 2 vers 3. 16% utilisent Python 3 comme VM principale, contre 81% pour 2.7 (on notera la part ridicule des autres versions, que vous pouvez donc ignorer dans le support de vos libs). Le report de la dépréciation de Python 2 de 2015 à 2020 a donc tout son sens : la communauté a pris son temps car le jeu n’en valait pas la chandelle.

Mais on y arrive. La plupart des libs importantes sont portées, les hostings supportent beaucoup mieux Python 3, même le portage de twisted a repris, grace à Crossbar d’ailleurs :) Perso je commence toujours mes nouveaux projets en Python 3.

On peut le dire maintenant, tout le monde a eu un peu peur. D’ailleurs Guido n’est pas prêt de recommencer. Après PHP 6 et Perl 6 qu’on attend toujours, des ralages dans tous les sens, des version 3.0, 3.1 et 3.2 vraiment à chier, après le retrait de % pour les bytes et l’appel pour une 2.8, on s’est tous demandé si ça allait pas foirer.

Maintenant, je suis certain que non. On a passé cette étape, ça a pris du temps, ça a fait mal au cul, mais pendant que Node se divise en 3 versions alors qu’il existe depuis à peine 6 ans, Python fête ses 20 ans avec une seule transition difficile, qui s’est révélée gagnante.

Mais gagnante pour qui ?

C’est vrai qu’il y a de bonnes features qui ne sont qu’en version 3 : asyncio, pathlib, statistics, ipaddress, enums, lzma, raise from, yield from, singledispatch, multiprocessing.Pool.map, @, await/async, les type hints, le support avancé des closures et l’unpacking étendu. Mais d’abord, certaines arrivent pour la 3.5, ensuite, les autres n’ont visiblement pas motivé les vieux de la vieille à faire le pas pendant 5 ans.

Pourquoi ? Parce que Python 3 n’a pas été créé pour eux. Il a été créé pour les nouveaux arrivants dans le langage.

Ayant un peu d’expérience avec le fait d’enseigner Python, je peux vous dire que faire passer Python 2 et Python 3, c’est le jour et la nuit :

Nous, utilisateurs chevronnés de la 2.7, ne nous rendons pas compte de la somme de connaissance que nous avons accumulé au fil des années pour contourner ces problèmes. Les nouveaux, même autodidactes, vont aller beaucoup plus vite.

Python 3 fait très bien son boulot : garder Python sa place de meilleur langage pour l’apprentissage. Ca tombe bien, parce qu’un fois appris, il sert aussi à plein de choses professionnellement, et pas juste du dev Web (ruby), du sysadmin (perl) ou des maths (R).

Et du coup la migration a pris du temps.

Depuis la 3.4, tout s’accélère. L’investissement est en train de payer.

Perso, quand je dois écrire du Python 2 maintenant, ça me fait râler :)