Sam & Max » unit tests 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
Un gros guide bien gras sur les tests unitaires en Python, partie 4 8 http://sametmax.com/un-gros-guide-bien-gras-sur-les-tests-unitaires-en-python-partie-4/ http://sametmax.com/un-gros-guide-bien-gras-sur-les-tests-unitaires-en-python-partie-4/#comments Sat, 06 Dec 2014 20:34:40 +0000 http://sametmax.com/?p=12717 Après avoir vu pytest, un outil typiquement pythonique sont les doctests, des tests unitaires intégrés dans les docstrings.]]> Python est un langage très pro, et il y a beaucoup, beaucoup d’outils pour faire des tests.

Après avoir vu pytest, un outil typiquement pythonique sont les doctests, des tests unitaires intégrés dans les docstrings.

Pour rappel, les docstrings, ce sont ces chaînes de caractères qu’on retrouve au début des modules, sous la signature des fonctions ou dans la définition des classes. Elles servent à la documentation de ceux-ci, ainsi on peut la lire dans le code, et dans les vraies docs car les outils standards savent les extraire.

Ça ressemble à ça :

def une_fonction():
    """ Ceci est une docstring.
 
        On peut la lire dans le code source, avec help() dans le shell ou
        dans les docs générés par pydoc et sphinx.
    """
    pass

Et bien ces docstrings, on peut mettre des tests unitaires dedans formatés comme des sessions de shell Python. Cela permet de donner des exemples d’usage, tout en testant son code. C’est chouette.

Musique ?

Musique.

Hello doctests

Faire des doctests n’est pas bien compliqué car c’est du copier coller. On fait une session shell avec ce qu’on veut tester, et on copie-colle le tout dans la docstring. Fastoche.

# on copie juste la session de shell tel quel
def ajouter(a, b):
    """
        >>> ajouter(1, 2)
        3
    """
    return a + b
 
# et on demande à Python de parser les doctests. Directement dans votre fichier
# de code. Si, si. Pas de fichier de tests à part.
if __name__ == "__main__":
    import doctest
    doctest.testmod()

On lance ensuite directement notre fichier de code :

python mon_module.py

Et ça n’affiche absolument rien. C’est parce qu’il n’y a pas d’erreur. On peut avoir le topo en demandant un peu de verbosité avec -v :

python mon_module.py -v
Trying:
    ajouter(1, 2)
Expecting:
    3
ok
1 items had no tests:
    __main__
1 items passed all tests:
   1 tests in __main__.ajouter
1 tests in 2 items.
1 passed and 0 failed.
Test passed.

Les doctests marchent purement en se basant sur le formatage texte. Python va prendre la ligne avec >>>, l’exécuter, si la ligne suivante ne contient pas de >>>, il va comparer le résultat de l’exécution de la ligne précédente avec le contenu de la ligne qui la suit.

Ceci passe :

"""
    >>> ajouter(1, 2)
    3
"""

Mais ceci échoue :

"""
    >>> ajouter(1, 2)
    4
"""

Car le résultat AFFICHÉ dans le shell est 3, et non 4.

En cas d’échec, Python vous en dit un peu plus :

python mon_module.py
**********************************************************************
File "mon_module.py", line 6, in __main__.ajouter
Failed example:
    ajouter(1, 2)
Expected:
    4
Got:
    3
**********************************************************************
1 items had failures:
   1 of   1 in __main__.ajouter

Formater ses doctests

Les doctests sont faits pour s’intégrer de manière transparente aux docstrings. On peut donc en mettre autant qu’on veut, au milieu du texte ordinaire de la docstring. Python se base sur les chevrons (>>>) pour savoir quand commence un test, et le saut de ligne pour savoir quand ça se termine. Au delà de ça, le style est libre.

def ajouter(a, b):
    """ Je peux mettre n'importe quoi ici.
 
        Et ici aussi.
 
        Puis intégrer des tests:
 
        >>> ajouter(1, 2)
        3
        >>> ajouter(2, 2)
        4
 
        Et un saut de ligne indique que les tests sont terminés. Mais je peux
        encore en ajouter après si je veux.
 
        >>> ajouter(0, 0)
        0
 
    """
    return a + b

Néanmoins, l’intérêt des doctests est de documenter son code à travers les tests, et donc on adoptera généralement un format tel que :

def ajouter(a, b):
    """ Additionne deux elements.
 
        Exemple :
 
            >>> # on peut mettre des commentaires ici
            >>> ajouter(1, 2) # ou là
            3
            >>> ajouter(2., 2) # fonctionne sur tous les types de nombre
            4.0
 
        La fonction fonctionne en duck typing, et accepte donc tout objet
        qui possède la méthode magique __add__ :
 
            >>> ajouter('a', 'b')
            'ab'
            >>> ajouter([1], [2])
            [1, 2]
    """
    return a + b

Notez comme il est agréable de lire cette docstring : on comprend tout de suite comment utiliser la fonction. En prime, ce sont des tests unitaires qui garantissent que notre fonction va continuer de fonctionner correctement et nous oblige à garder cette doc à jour.

On peut faire des imports dedans ou utiliser temporairement pdb pour debugger. N’importe quel code de shell est valide mais faites attention à ne pas démarrer des boucles infinies comme les event loops des GUI ou lib async.

Voici ce que donnerait l’exemple des articles précédents avec des docstests :

def get(data, index, default=None):
    """ Implémente l'équivalent de dict.get() pour les indexables.
 
        Example :
 
            >>> simple_comme_bonjour = ('pomme', 'banane')
            >>> get(simple_comme_bonjour, 0)
            'pomme'
            >>> get(simple_comme_bonjour, 1000, 'Je laisse la main')
            'Je laisse la main'
    """
    try:
        return data[index]
    except IndexError:
        return default

Problèmes et solutions

Les doctests ne sont pas la Panacée, particulièrement parce que le test se fera sur le résultat AFFICHÉ dans le shell. Cela peut facilement amener à des erreurs.

Déjà, il faut faire attention à la représentation des objets dans le shell Python. La représentation n’est pas forcément la valeur de saisie :

>>> 1.
1.0
>>> "1"
'1'
>>> {"foo": "bar", "une apostrophe : '": "est échapée ainsi qu'un accent"}
{"une apostrophe : '": "est \xc3\xa9chap\xc3\xa9e ainsi qu'un accent", 'foo': 'bar'}

La solution à ce problème est de tester dans le shell les valeurs de retour, et non de le faire de tête. Faites bien gaffe aux espaces qui sont donc significatifs, surtout ceux en fin de ligne. Mon éditeur est configuré pour les virer par défaut, et ça m’a niqué en écrivant l’article :)

Ensuite, il y a des cas où la représentation ne sera pas la même d’un appel à l’autre.

C’est le cas avec les dictionnaires, puisque l’ordre des éléments n’est pas garanti par nature. Ne faites donc pas :

>>> retourne_un_dico()
{'ordre': 'non garanti', 'le': 'resultat'}

Mais plutôt quelque chose qui vous garantit l’affichage :

"""
>>> res = list(retourne_un_dico().items())
>>> res.sort()
[('le', 'resultat'), ('ordre', 'non garanti')]
>>> # ou
>>> retourne_un_dico() == {'ordre': 'non garanti', 'le': 'resultat'}
True
"""

Parfois, on ne peut juste pas garantir l’affichage. Par exemple avec des nombres non prévisibles comme les hash ou les id des objets :

"""
>>> class Test(): pass
>>> repr(Test())
''
"""

7f4687d30fc8 n’est ici pas prévisible. Python met certains cas spéciaux comme celui-ci des flags activables via le commentaire # doctest: +NOM_DU_FLAG.

Par exemple, le flag ELLIPSIS permet de placer ... dans le résultat en guise de joker :

"""
>>> repr(Test()) # doctest: +ELLIPSIS
''
"""

D’autres problèmes similaires peuvent être résolus ainsi. Le flag SKIP permet de sauter un test que vous voulez mettre là, en exemple, mais qui ne doit pas être testé :

"""
>>> # ce test va être ignoré
>>> repr(Test()) # doctest: +SKIP
''
"""

NORMALIZE_WHITESPACE permet de considérer toute séquence de caractères non imprimables comme un espace. 8 tabs ou 4 espaces dans le résultat seront tous considérés comme un espace.

"""
>>> 'ceci est une assez longue ligne divisible' # doctest: +NORMALIZE_WHITESPACE
'ceci    est     une assez longue    ligne divisible'
"""

Les flags sont cumulables, si on les sépare par des virgules dans le commentaire.

Autre astuce, si votre sortie doit contenir un saut de ligne, Python va l’interpréter comme la fin des tests. On peut pallier cela en utilisant <BLANKLINE> :

"""
>>> print('Un saut de ligne\\n')
Un saut de ligne
 
"""

Faites attention aux antislash et autres caractères spéciaux dans vos docstests puisque toute string est parsée deux fois : une fois à l’écriture de la docstring, puis une fois à son exécution. Ici vous voyez que je suis tenu d’échapper mon \n On peut d’ailleurs utiliser les préfixes r (cf: les raw strings) et u sur les docstrings, si un jour vous êtes bloqué par trop d’échappements ou des caractères non ASCII en pagaille, pensez-y.

Un cas particulier est celui des exceptions. LOL, n’est-il pas ?

Pour y répondre, Python décide qu’une expression est levée si il voit Traceback (most recent call last):. Il ignore ensuite tout le texte – qui est donc optionnel et que vous pouvez omettre – jusqu’à ce qu’il rencontre le nom de l’exception levée. À partir de là, il vérifie que le test passe.

Par exemple, si votre exception génère ce traceback :

Traceback (most recent call last):
  File "", line 1, in 
  File "test.py", line 41, in ajouter
    1 / 0
ZeroDivisionError: integer division or modulo by zero

Vous pouvez faire dans votre doctest :

"""
>>> je_leve_une_exception()
Traceback (most recent call last):
ZeroDivisionError: integer division or modulo by zero
"""

Seule la dernière ligne est comparée.

Il est également possible de mettre les doctests dans un fichier texte à part, mais je ne vous le recommande pas. Cela retire l’intérêt principal des doctests : avoir du code exécutable dans la doc. Si on doit avoir un fichier séparé, autant utiliser des tests normaux, bien plus pratiques et complets.

Car il n’y a pas de tear down, setup ou fixtures avec les docstests. Ca reste un outil basique.

Sachez néanmoins que les doctests sont parfaitement compris par pytest, il suffit juste de lui demander de les exécuter avec l’option suivante :

py.test --doctest-modules

Dans ce cas, il n’est pas nécessaire de faire à la fin de chaque fichier contenant des doctests :

if __name__ == "__main__":
    import doctest
    doctest.testmod()

Quand utiliser les doctests ?

Généralement, on utilise un mélange des tests ordinaires (dans notre cas des tests pytest plutôt que unittest) et des doctests.

On utilisera des doctests pour les objets ou les fonctions simples et indépendantes. J’entends par là, des fonctions et des objets qui prennent uniquement des types basiques en paramètres, et qui retournent uniquement ces types basiques en paramètres. Pour les objets, ils doivent avoir peu de méthodes.

Pour tout le reste, on utilisera des tests ordinaires.

Par exemple, si vous avez une fonction comme notre exemple get(), les doctests sont un bon choix. En revanche, si vous avez un objet Server qui est un serveur HTTP, ou une fonction qui prend un objet Server en paramètre, il vaut mieux utiliser les tests ordinaires.

Il est tout à fait possible, et même agréable, de mettre quelques tests simples en doctests qui aident à la documentation, et de faire les tests les plus compliqués via pytest.

Prochaine étape, les mocks. Parti de là, je pourrai vous dire quelles parties de votre programme tester en priorité, et comment. Au début je voulais faire l’inverse, mais finalement, c’est plus pratique.


Télécharger le code de l’article

]]>
http://sametmax.com/un-gros-guide-bien-gras-sur-les-tests-unitaires-en-python-partie-4/feed/ 8
Un gros guide bien gras sur les tests unitaires en Python, partie 1 19 http://sametmax.com/un-gros-guide-bien-gras-sur-les-tests-unitaires-en-python-partie-1/ http://sametmax.com/un-gros-guide-bien-gras-sur-les-tests-unitaires-en-python-partie-1/#comments Wed, 15 Jan 2014 15:26:00 +0000 http://sametmax.com/?p=8764 La zik maintenant traditionelle :

Les tests unitaires font partie de ces “bonnes pratiques” que tout le monde semble appliquer sur le net. Tous les devs hypes parlent de tests unitaires : les conférences, les blogs, les tutos, les livres, whooooo !

Dans la vraie vie vivante, on croise pourtant peu de gens qui les utilisent vraiment. On les retrouvent surtout dans les gros projets et les grosses boîtes, et encore.

Il y a plusieurs raisons à cela. D’une part, beaucoup, beaucoup, beaucoup de développeurs n’ont aucune idée de ce qu’est un test unitaire. Ceux qui savent, ne voient pas forcément l’intérêt, et ceux qui en voient l’intérêt n’ont pas forcément l’expérience nécessaire à leur mise en œuvre.

Je connais des tas de dev qui codent des tas d’excellents projets sans le moindre test unitaires.

L’adage selon lequel un code sans test unitaire est un code buggé est parfaitement faux puisque existe bien d’autres manières de tester son code. De plus, même un code bien testé est un code buggé. Je le sais, je l’ai codé.

Malgré cela, vous devriez maitriser l’usage des tests unitaires, car quand vous arrivez à vous sortir les extrémités digitales de la terminaison dorsale afin de les mettre en place, le bénéfice est très important. Mais aussi parce que certains projets ne peuvent pas s’en passer, et donc que vous ne pourrez pas travailler dessus sans savoir en faire. Certains projets sur Github n’acceptent pas de pull request sans couverture de tests, et certaines personnes n’utiliseront pas votre lib si elle n’est pas testée. C’est un gage de qualité.

Je n’en ferai pas une question morale ou de principe, les projets que l’on publie sur Sam et Max sont parfaitement exempt de tests unitaires, et d’ailleurs, la plupart des projets pros avec Max n’ont aucun tests non plus.

En revanche, en tant que freelance, je prends généralement le temps d’en faire.

Pas de dogmatisme du test donc, mais passé le goût de crabe dans la bouche, ça vaut le coup, alors lisez ce guide.

Qu’est-ce qu’un test unitaire

Le test unitaire est un bout de code qui fait exactement ce que son nom dit : il teste une unité de code.

Le problème c’est quoi tester, qu’est-ce qu’une “unité de code”, ce n’est pas quelque chose d’évident à définir, et vient avec la pratique. En théorie c’est un bout de code minimaliste, que l’on ne peut pas réduire plus. En pratique, on choisit avec pragmatisme un truc assez petit, mais pas trop, parce que merde, hein.

Mais alors que veut-on dire par “tester” ?

Et bien c’est d’une banalité affligeante : on donne des entrées au code, et on vérifie que ses sorties sont celles attendues pour ces entrées.

Bref, généralement (mais pas toujours) on teste une fonction. Souvent avec une autre fonction. Et c’est d’un manque d’originalité terrible.

Le test unitaire le plus bête qu’on puisse avoir en Python :

# Fichier de code
def fonction_a_tester(param1, param2):
    return param1 + param2
# Fichier de test
 
from fichier_de_code import fonction_a_test
 
assert fonction_a_tester(1, 1) == 2  # test de l'addition
assert fonction_a_tester(1, -1) == 0 # test avec chiffre négatif
assert fonction_a_tester(4, 2) == 6 # test avec autre chose que des 1
assert fonction_a_tester(4.5, 2) == 6.5 # test avec des floats

Deux constats :

  • C’est parfaitement chiant. Les tests unitaires sont dans 99% des cas des tautologiques super ennuyeuses.
  • On teste le même code plusieurs fois, avec plusieurs cas de figure, pour être certain que ça se comporte comme prévu.

assert est un mot clé qui lève l’exception AssertionError quand l’expression évaluée ne retourne pas True. L’utilisation d’assert n’est pas le sujet de l’article, ici on s’en sert pour faire un test unitaire tout simplement parce que la première ligne qui ne renverra pas True fera planter le programme. C’est le test unitaire du pauvre.

Un test unitaire, ce n’est que ça. Un répétition bête et emmerdante de vérifications généralement très connes.

C’est minable ! A quoi ça sert ?

Là normalement vous vous dites “je sais ce que fait mon code, surtout une unité minimaliste, je n’ai pas besoin d’écrire des évidences pour le tester”. Et c’est pour cela que je ne suis pas dogmatique sur les tests unitaires, car c’est en partie vrai. Beaucoup de codes sont suffisamment simples ou peu critiques pour ne pas avoir besoin d’être renforcés par des tests unitaires. Et même si il faut des tests, tout le code n’a pas nécessairement besoin d’être testé.

Lancer un blog pour sa cousine n’est pas la même chose qu’une site de rencontre pour un grand compte.

Mais le test unitaire a plusieurs bénéfices. Le premier c’est qu’il vous oblige à réfléchir aux entrées et sorties de vos fonctions, et à l’API de votre code en général. Vous vous apercevrez à l’usage qu’un code est plus ou moins facile à tester selon la manière dont vous l’avez organisé, et ce faisant, vous serez forcé d’écrire un code plus souple, propre, extensible.

Écrire des tests fait de vous un meilleur développeur.

Cependant ce n’est pas le principal intérêt. Le véritable gain tient dans ce que vous gagnez dans le futur : quand vous allez modifier votre code, vous pourrez rapidement voir si il n’est pas cassé. En effet, votre code va grossir, et vous ne vous souviendrez pas de toutes les dépendances, de tous les effets de bords, de toutes les interactions. Certains dev sont meilleurs que d’autres à tout garder dans la tête, mais même Cortex a ses limites. Au bout d’un moment, le code est plus fort que vous.

À partir de là, vous allez tout de même avoir besoin de factoriser le code, bouger des choses, en ajouter d’autres, corriger un bug, faire un petit ajustement. À chaque fois que vous le faites, vous prenez le risque de casser un truc. Au début du projet, le risque est faible, et même si ça arrive, ça se répare vite. Après 2 mois de dev, les tests seront votre filet de sécurité. Vous pouvez les lancer après chaque modif, et voir que vous n’avez rien pété. Vous pouvez les lancer après une contribution d’un autre dev, et voir que ça tourne toujours. Vous pouvez les lancer après un changement d’environnement (OS, base de données, système de fichier, format, etc) et vous assurer que ça n’a pas d’impacts.

Particulièrement, des tests unitaires ont beaucoup de valeur sur un projet avec beaucoup de participants, tels que des logiciels libres populaires ou des systèmes de grandes sociétés.

Par exemple, sur notre dernière fonction bidon, on décide de faire une petite modification :

# Fichier de code
def fonction_a_tester(param1, param2):
    return int(param1) + int(param2)

On peut maintenant passer une string, et elle sera convertie en entier.

On lance notre batterie de tests, et là, au milieu de centaines d’autres tests, celui là foire :

assert fonction_a_tester(4.5, 2) == 6.5 # test avec des floats

On voit très vite que notre idée était pourrie, car on a un use case qui ne sera plus compatible. Si quelqu’un a utilisé des floats avec notre fonction, on va casser son code.

En l’essence, c’est ça l’intérêt des tests unitaires : vous faire sauter au yeux quand quelque chose casse. On appelle ça des “tests de régression”, et c’est l’usage le plus courant.

Plus tard vous verrez qu’on utilise aussi les tests pour développer son code (TDD), pour définir un comportement du produit avec le client (BDD) ou tout simplement pour servir de documentation.

Mais l’usage de base, c’est ça. S’assurer qu’on est pas en train de merder.

Résumé

  1. N’écoutez pas les Papes du test vous disant que si vous n’avez pas des tests unitaires à 50 ans, vous avez raté votre vie. Les tests, c’est bien. Un projet livré, c’est mieux. Une documentation est plus importante que des tests. Les 3, évidement, c’est l’idéal.
  2. Un test, c’est une suite parfaitement chiante d’énonciations d’évidences. Il n’y a généralement rien de compliqué dans les tests. Vous vous sentirez parfois insulté en les écrivant tellement c’est con.
  3. L’intérêt majeur des tests est d’avoir une alerte rouge qui se lance quand vous avez pété un truc. Ça arrive bien plus souvent que vous ne le croyez sans que vous ne vous en aperceviez car vous n’avez pas de tests.

Ces bases posées, la prochaine partie fera la démonstration du module unittest afin de créer vos premiers tests unitaires en Python, puis on enchaînera, partie par partie, sur les applications pratiques, les variantes, les girafes lesbiennes et tout ce qui fait un bon article de s&m.

Dans la partie 2, on va voir comment faire des tests en utilisant la lib standard Python.

]]>
http://sametmax.com/un-gros-guide-bien-gras-sur-les-tests-unitaires-en-python-partie-1/feed/ 19
Ajouter une route au urls.py de Django durant les tests http://sametmax.com/ajouter-une-route-au-urls-py-de-django-durant-les-tests/ http://sametmax.com/ajouter-une-route-au-urls-py-de-django-durant-les-tests/#comments Thu, 09 May 2013 08:55:37 +0000 http://sametmax.com/?p=5965 Créer des routes dans l’urlconf juste pour les tests unitaires n’est pas très propre, heureusement on peut utiliser des urls de test complètement séparées.

D’abord, il faut mettre un fichier urls.py dans votre dossier tests qui contient vos routes de tests. Ensuite…

Si vous utilisez le mécanisme de Django pour les tests, il suffit de déclarer le chemin de ce module comme attribut urls de votre classe de test, et Django se charge du reste :

class VotreTestCase(TestCase):
    urls = 'votreapp.tests.urls'

Dans le cas où, comme moi, vous préférez utiliser une autre lib pour écrire vos tests, vous pouvez quand choisir votre module d’urls en faisant :

from django.conf import settings
from django.core.urlresolvers import clear_url_caches
 
import urls
 
settings.ROOT_URLCONF = urls
# n'oubliez pas cette ligne, sinon vous autre des comportements aberrant
# car django met les urls en cache
clear_url_caches()
]]>
http://sametmax.com/ajouter-une-route-au-urls-py-de-django-durant-les-tests/feed/ 0
Paramètres par défaut pour la commande py.test 3 http://sametmax.com/parametres-par-defaut-pour-la-commande-py-test/ http://sametmax.com/parametres-par-defaut-pour-la-commande-py-test/#comments Fri, 03 May 2013 08:23:51 +0000 http://sametmax.com/?p=5969 pytest, et je me retrouve souvent à rentrer les mêmes paramètres de la commande encore et encore. Parfois, quand j'autilise des wrappers tels que django-pytest et pytest-django (ça s'invente pas), je ne peux même pas passer d'arguments directement à py.test. On peut y remedier en créer un fichier de config à la racine du projet.]]> Je ne fais plus de tests unittaires sans pytest, et je me retrouve souvent à rentrer les mêmes paramètres de la commande encore et encore. Parfois, quand j’utilise des wrappers tels que django-pytest et pytest-django (ça s’invente pas), je ne peux même pas passer d’arguments directement à py.test.

On peut y remédier en créant un fichier de config à la racine du projet. Nommez le fichier tox.ini, car c’est aussi le nom du fichier de configuration de l’outil de tests tox, et il est compatible, donc autant avoir un seul format. Dedans, créez une section pytest, et vous pouvez configurer la lib la dedans.

En l’occurrence, le settings “addopts”, pour “add options” (ajouter options), permet de spécifier les options de la ligne de commande à toujours ajouter à py.test.

Mon fichier contient toujours au moins ceci :

[pytest]
addopts = --ignore="virtualenv" --capture=no

Ainsi py.test ignore toujours le dossier virtualenv (qui est un lien vers l’env virtuel de mon projet) car je ne veux pas qu’il lance les tests de ce dossier. Et il ne capture pas stdout, ce qui me permet d’utiliser ipdb pendant les tests unittaires. Parfois j’utilise aussi --maxfail=1 quand je veux qu’il s’arrête dès la première erreur rencontrée.

Pour ne pas aller dans ce dossier et ne pas capturer stdout.

]]>
http://sametmax.com/parametres-par-defaut-pour-la-commande-py-test/feed/ 3
Se simplifier les tests Python avec Pytest 11 http://sametmax.com/se-simplifier-les-tests-python-avec-pytest/ http://sametmax.com/se-simplifier-les-tests-python-avec-pytest/#comments Wed, 07 Nov 2012 12:04:50 +0000 http://sametmax.com/?p=2884 assert, mais un résultat plus clair que unittest en sortie ?]]> Personne n’aime faire des tests unitaires. C’est un peu comme les impôts: on sait que c’est utile, mais on est jamais content de s’en occuper.

Réchèr m’a dernièrement posé la question de l’abondance des méthodes assertTruc() et leur utilité, et je lui ai répondu que chaque méthode donnait des infos adaptées au test effectué.

Max m’a dernièrement fait la remarque que les tests “c’est bien mais c’est compliqué”. J’avoue être à court de contre argument.

Et si on pouvait rendre les tests plus simples à écrire et à lire, aussi simple qu’un assert, mais avec un résultat plus clair que unittest en sortie ?

pip install pytest

Pytest est une lib de test à utiliser à la place de unittest. Ses créateurs utilisent l’introspections et l’injection de dépendance pour créer des tests magiquement.

D’ordinnaire, la magie, on aime pas trop ça en Python, et on laisse ça aux rubistes. Mais dans le domaine du test, qui n’est pas un code de production avec les mêmes contraintes de lecture, de recherche de bugs architecturaux et d’interactions entre dev, mais qui a par contre une forte contrainte “j’ai pas envie d’écrire un caractère de plus”, ça a du sens.

Voilà comment ça se passe: on vire toute ce qui est classe et setup verbeux. On laisse juste les imports de vos libs, et les tests. Avec des assert. Pytest va alors analyser tout ça, et faire tout le boulot autour pour vous.

Exemple:

Dans votre lib:

def ma_fonction_a_tester(a, b):
    return a + b

Dans votre fichier test.py:

from malib import ma_fonction_a_tester
 
def test_function():
    assert ma_fonction_a_tester(1, 1) == 2

Et on lance :

py.test test.py

Pour obtenir:

====== test session starts ======
platform linux2 -- Python 2.7.3 -- pytest-2.3.2
collected 1 items

Bureau/test.py .

====== 1 passed in 0.02 seconds ======

Et voilà, les tests redeviennent bêtes et simples. Mais ils ne perdent pas en puissance. Car Pytest analyse le assert, et le transforme à la volée. Du coup, pour les structures de données complexes, Pytest va vous sortir les infos de debug utile que assertTruc() de unittest vous aurait sorti.

Exemple avec des tuples:

def ma_fonction_a_tester(a, b):
    return (a * 2, b * 2)
 
 
def test_function():
    assert ma_fonction_a_tester(1, 1) == (2, 2, 3)

Va donner:

====== test session starts ======
platform linux2 -- Python 2.7.3 -- pytest-2.3.2
collected 1 items

Bureau/test.py F

====== FAILURES ======
______ test_function ______

    def test_function():
>       assert ma_fonction_a_tester(1, 1) == (2, 2, 3)
E       assert (2, 2) == (2, 2, 3)
E         Right contains more items, first extra item: 3

Bureau/test.py:7: AssertionError
====== 1 failed in 0.02 seconds ======

On nous indique clairement qu’il y a un item de trop dans mon résultat, et lequel.

En prime, Pytest nous affranchie des fonctions setUp() et tearDown() génériques. Le problème de ces méthodes dans unittest, c’est qu’elles sont éxécutées à chaque début de test. On en a pas forcément besoin, et on a pas les mêmes besoins pour chaque test.

Pytest ajoute encore un peu de magie pour régler le probleme

Dans votre lib, vous avez ça:

import re
 
def extraire_title(html):
    """
        Extrait le title d'une page HTML a base de regex. C'est mal.
    """
    try:
        return re.search(r'<title[^>]*>(.*)</title>', html).groups()[0]
    except IndexError, AttributeError:
        return None

Dans votre fichier de tests, vous aurez:

import urllib2
 
import pytest
 
@pytest.fixture
def exemple_html():
    return urllib2.urlopen('http://www.google.com').read()
 
def test_extraire_title(exemple_html):
    assert extraire_title(exemple_html) == 'Google'

Qu’est-ce qui va se passer ?

exemple_html() va être déclarée comme une “fixture”, c’est à dire quelque chose qui contient ou génère des données de tests.

Quand Pytest va lancer les tests, il va voir qu’un argument de test_extraire_title() porte le même nom que la fonction exemple_html. Alors, il va automatiquement appeler exemple_html(), et passer le résultat à test_extraire_title() pour lancer le test.

On peut donc avoir des tas de fonctions de setup, partagées entre plein de fonctons de tests.

]]>
http://sametmax.com/se-simplifier-les-tests-python-avec-pytest/feed/ 11
Quelques innovation de Python 3 backportées en Python 2.7 4 http://sametmax.com/quelques-innovation-de-python-3-backportees-en-python-2-7/ http://sametmax.com/quelques-innovation-de-python-3-backportees-en-python-2-7/#comments Mon, 05 Nov 2012 11:36:42 +0000 http://sametmax.com/?p=2863 Comme nous l’avons vu avec les vues ou les collections, Python 2.7 vient avec pas mal de bonus issus directement de la branche 3. En voici quelques autres. Tout ceci n’est bien sûr ni nouveau ni exhaustif, mais je m’aperçois que peu de personnes le savent.

Une notation littérale pour les sets:

>>> {1, 2} == set((1, 2))
True

Une syntaxe pour les dictionnaires en intention:

>>> d = {chr(x): x for x in range(65, 91)}
>>> d
{'A': 65, 'C': 67, 'B': 66, 'E': 69, 'D': 68, 'G': 71, 'F': 70, 'I': 73, 'H': 72, 'K': 75, 'J': 74, 'M': 77, 'L': 76, 'O': 79, 'N': 78, 'Q': 81, 'P': 80, 'S': 83, 'R': 82, 'U': 85, 'T': 84, 'W': 87, 'V': 86, 'Y': 89, 'X': 88, 'Z': 90}

Imbriquer with:

Avant il fallait utiliser nested() ou imbriquer à la main

with open('fichiera') as a:
    with open('fichiera') as b:
        # faire un truc

Maintenant on peut faire:

with open('fichiera') as a, open('fichiera') as b:
    # faire un truc

Rien à voir, mais toujours sympa. timedelta a maintenant une méthode total_seconds() qui retourne la valeur de la durée en seconde. En effet, l’attribut seconds ne retourne que ce qui reste en seconde une fois qu’on a retiré les jours:

>>> from datetime import timedelta
>>> delta = timedelta(days=1, seconds=1)
>>> delta.seconds
1
>>> delta.total_seconds()
86401.0

Notez qu’il n’y a toujours ni attribut minutes, ni heures.

Le module unittest gagne une pléthore d’améliorations, et notamment:

L’utilisation de assertRaises comme context manager:

with self.assertRaises(KeyError):
    {}['foo']

Et un bon gros nombres de méthodes:

assertIsNone() / assertIsNotNone(), assertIs() / assertIsNot(), assertIsInstance() / assertNotIsInstance(), assertGreater() / assertGreaterEqual() / assertLess() / assertLessEqual(), assertRegexpMatches() / assertNotRegexpMatches(), assertRaisesRegexp(),
assertIn() / assertNotIn(), assertDictContainsSubset(), assertAlmostEqual() / assertNotAlmostEqual().

Enfin format() commence à devenir une alternative valable à % car il propose maintenant des marqueurs sans noter d’index:

>>> "{}, puis {} et finalement {}".format(*range(3))
'0, puis 1 et finalement 2'

Et il ajoute le séparateur des milliers au mini-langage de formatage, mais pour la virgule uniquement. Par exemple, si avoir un nombre de 15 caractères minimum formater en tant que float, avec deux chiffres après la virgules, et donc les milliers sont groupés à l’américaine:

>>> '{:15,.2f}'.format(54321)
'      54,321.00'
]]>
http://sametmax.com/quelques-innovation-de-python-3-backportees-en-python-2-7/feed/ 4
Répéter une commande bash jusqu’à ça marche ou que ça plante 5 http://sametmax.com/repeter-une-commande-bash-jusqua-ca-marche-ou-que-ca-plante/ http://sametmax.com/repeter-une-commande-bash-jusqua-ca-marche-ou-que-ca-plante/#comments Wed, 08 Aug 2012 14:37:01 +0000 http://sametmax.com/?p=1594 Parfois il est utile de spammer son terminal jusqu’à ce que mort s’en suive.

Par exemple, hier nous avions un bug aléatoire qui apparaissait dans une batterie de tests qui prenait quelques minutes à exécuter. Confirmer qu’on avait bien éliminé le problème était délicat.

On est donc allé faire les courses (c’est y pas mignon, le petit couple sam et max au super marché, so kawaï) et j’ai lancé ceci avant de partir faire la queue pendant une heure:

nosetests tests -x; while [[ $? -eq "0" ]]; do nosetests tests -x; done

nosetests tests -x lance tous les tests et plante dès le premier qui foire.

$? -eq "0" vérifie le dernier code de retour, et continue si il est égal à 0 (ce qui veut dire “tout s’est bien passé” en langue Unix).

Ainsi les tests ont tourné pendant toute notre session de chasse au rayon fruits et légumes, et à au retour on a pu constaté qu’ils tournaient toujours. Il y a donc de forte chance que le bug ai été éradiqué, puisqu’avant il se montrait dans les 5 premiers lancements.

On peut aussi faire l’inverse: lancer une commande tant qu’elle foire:

command; while [[ $? -gt "0" ]]; do command; done
]]>
http://sametmax.com/repeter-une-commande-bash-jusqua-ca-marche-ou-que-ca-plante/feed/ 5