unittest – Sam & Max http://sametmax.com Du code, du cul Wed, 30 Oct 2019 15:34:04 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 Comment mocker une coroutine ? http://sametmax.com/comment-mocker-une-coroutine/ http://sametmax.com/comment-mocker-une-coroutine/#comments Mon, 25 Jan 2016 10:34:23 +0000 http://sametmax.com/?p=17976 le guide sur les tests en python (que je dois toujours terminer, je sais...), je vous parle des objets mocks. Si vous avez eu le plaisir de jouer avec asyncio, vous avez du noter que unittest.mock n'a aucun outil pour gérer gérer les coroutines.]]> Dans le guide sur les tests en python (que je dois toujours terminer, je sais…), je vous parle des objets mocks.

Si vous avez eu le plaisir de jouer avec asyncio, vous avez du noter que unittest.mock n’a aucun outil pour gérer gérer les coroutines.

En attendant que ce soit intégré à la stdlib, voici une petite recette :

import asyncio
from unittest.mock import Mock

# on utilise toute la machinerie du Mock original
class AMock(Mock):


    def __call__(self, *args, **kwargs):
        # la référence du parent doit se récupérer hors
        # hors de la closure
        parent = super(AMock, self) 
        # sauf qu'à l'appel on créé une fonction coroutine
        @asyncio.coroutine
        def coro():
            # Qui fait le vrai Mock.__call__ (et donc popule l'historique 
            # des appels), mais seulement après l'évent loop l'ait éxécuté
            return parent.__call__(*args, **kwargs)

        # On appelle la fonction coroutine pour générer une coroutine
        # (les coroutines marchent comme les générateurs)
        return coro()


Je propose qu’en l’honneur de ce bidouillage, on l’appelle… mockoroutine !

Ca va s’utiliser comme ça:

mockorourine = AMock()
yield from mockorourine()

Après le yield from, mockorourine.call_count == 1, et mockorourine.assert_called_once_with()passe.

Si vous êtes en 3.5+, on peut même faire:

class AMock(Mock):

    def __call__(self, *args, **kwargs):
        parent = super(AMock, self)
        async def coro():
            return parent.__call__(*args, **kwargs)
        return coro()

    def __await__(self):
        # on delegue le await à la couroutine créée par __call__
        return self().__await__()

Puis:

await AMock()
]]>
http://sametmax.com/comment-mocker-une-coroutine/feed/ 6 17976
Est-ce que cet outil existe en Python ? http://sametmax.com/est-ce-que-cet-outil-existe-en-python/ http://sametmax.com/est-ce-que-cet-outil-existe-en-python/#comments Fri, 01 May 2015 08:35:33 +0000 http://sametmax.com/?p=16157 Le test unitaire le plus simple, c’est de vérifier que son API publique n’a pas changé.

Ça veut dire :

  • vérifier que les modules ont bien les mêmes définitions (un nom qui était importable n’a pas disparu).
  • vérifier que les fonctions et méthodes ont bien la même signature (les arguments avec le même nom n’ont pas changé de place, et les keywords arguments n’ont pas changé de nom).
  • vérifier que les classes ont le même nom, les mêmes méthodes publiques avec les mêmes signatures et les mêmes attributs.

Ce sont des tests super cons qui demandent d’écrire du code du genre :

class Foo:
    bar = True
    def __init__(self, stuff=False):
        self.stuff

    def thing(self):
        return "doh"

# et dans les tests
self.assertTrue(hasattr(Foo, 'bar'))
self.assertTrue(hasattr(Foo, 'thing'))
self.assertTrue(hasattr(Foo(), 'stuff'))

Et personne ne le fait, car déjà les tests c’est relou, mais écrire des tautologies à la main, c’est au dela du tolérable.

Pourtant, ce genre de tests m’aurait déjà sauvé les gosses plusieurs fois. Une faute de frappe qui se glisse, un attribut qu’on rend privé, un méthode qu’on déplace et on pense avoir tout refactoré, un pote qui change un truc qu’on utilisait, etc.

Je me demandais donc si il existait un truc qui permette de faire :

self.assertStablePublicAPI("package.module") # scanne tout le module et tous les objets
self.assertStablePublicAPI("package.module:Class") # check juste la classe
self.assertStablePublicAPI("package.module:func") # check juste la function

Le but étant que quand on lance les test, ça check si ça rencontre un nouvel objet/attribut/param et sauvegarde tout un dans un JSON. Aux prochains tests, si on a modifier l’API publique, le test foire jusqu’à ce qu’on modifie explicitement le fichier JSON.

On doit bien entendu pouvoir régler le truc pour ignorer ou ajouter explicitement des choses à checker, par exemple par défaut ça ignore tous les machins avec des underscores dans le nom.

J’ai cherché sur le net, mais j’arrive pas à trouver ça. Des pistes ?

]]>
http://sametmax.com/est-ce-que-cet-outil-existe-en-python/feed/ 13 16157
Se simplifier les tests Python avec Pytest 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 simples 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’introspection et l’injection de dépendance pour créer des tests magiquement.

D’ordinaire, 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 devs, 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 tout 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 utiles que assertTruc() de unittest vous aurait sorties.

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 affranchit des fonctions setUp() et tearDown() génériques. Le problème de ces méthodes dans unittest, c’est qu’elles sont exé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 problème:

Dans votre lib, vous avez ça:

import re

def extraire_title(html):
    """
        Extrait le title d'une page HTML à base de regex. C'est mal.
    """
    try:
        return re.search(r']*>(.*)', 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 fonctions de tests.

]]>
http://sametmax.com/se-simplifier-les-tests-python-avec-pytest/feed/ 13 2884