Sam & Max » mock http://sametmax.com Du code, du cul Sat, 07 Nov 2015 10:56:13 +0000 en-US hourly 1 http://wordpress.org/?v=4.1 Un gros guide bien gras sur les tests unitaires en Python, partie 5 9 http://sametmax.com/un-gros-guide-bien-gras-sur-les-tests-unitaires-en-python-partie-5/ http://sametmax.com/un-gros-guide-bien-gras-sur-les-tests-unitaires-en-python-partie-5/#comments Sun, 17 May 2015 07:35:20 +0000 http://sametmax.com/?p=16281 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é.]]> 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
>>> 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.

]]>
http://sametmax.com/un-gros-guide-bien-gras-sur-les-tests-unitaires-en-python-partie-5/feed/ 9
Générer des données factices avec faker 12 http://sametmax.com/generer-des-donnees-factices-avec-faker/ http://sametmax.com/generer-des-donnees-factices-avec-faker/#comments Thu, 17 Apr 2014 08:30:21 +0000 http://sametmax.com/?p=10029 faker fait partie de ses libs que j’ai toujours voulu écrire sans jamais prendre le temps de le faire. Comme arrow par exemple. Et puis un jour quelqu’un le fait, et je suis à la fois soulagé de ne pas avoir tenté de le faire (au risque de ne pas réussir aussi bien) et un peu déçu d’être passé à côté de la bonne idée.

Le principe de la lib est très simple : générer des données bidons. Noms, numéros de téléphone, adresses physiques ou email… C’est utile pour tout un tas de choses :

  • Faire des tests, évidement.
  • Créer des bots, des crawlers et tout autre programme qui doit se faire passer pour un utilisateur.
  • Remplir une base de données vide en attendant que les utilisateurs réels remplissent le site. Cela évite le sentiment d’arriver sur un service désert. Tous les sites de rencontre font ça.
  • Générer un contexte artificiel, par exemple pour un jeu vidéo.

Faker est déjà très simple à utiliser :

>>> from faker import Faker
>>> f = Faker()
>>> f.phone_number()
u'+69(4)8833689405'
>>> f.phone_number()
u'1-201-240-9452'
>>> f.phone_number()
u'+95(8)7680219065'
>>> f.phone_number()
u'754-833-9664x654'
>>> f.email()
u'bentley.gaylord@batz.org'
>>> f.email()
u'ludwig.rohan@adamskoch.info'

Et surtout déjà très riche. Il y a plus d’une centaine de choses que vous pouvez générer. Quelques exemples :

  • f.city
  • f.company
  • f.credit_card_number
  • f.date_time_this_decade
  • f.geo_coordinate
  • f.hex_color
  • f.ipv4
  • f.language_code
  • f.md5
  • f.mime_type
  • f.month_name
  • f.paragraph
  • f.password
  • f.postcode
  • f.ssn
  • f.street_address
  • f.url
  • f.user_agent
  • f.word

faker est très flexible. En effet, beaucoup d’éléments existent en plusieurs versions ou de manière composée et utilisables séparément. Ainsi, on peut prendre l’IPV4 ou V6, choisir l’adresse entière ou le code postal seul, récupérer le nom complet, ou juste le prénom :

>>> f.name()
u'Olin McCullough'
>>> f.first_name()
u'Brandie'

Mais en plus, faker est localisé :

>>> f = Faker(locale="fr_FR")
>>> f.name()
u'Martine de la Petitjean'

Enfin, on peut créer ses propres générateurs et fournisseurs de données au besoin.

Comme dirait Max, c’est d’la balle baby !

]]>
http://sametmax.com/generer-des-donnees-factices-avec-faker/feed/ 12