web-dev-qa-db-fra.com

Méthodes de simulation sur n'importe quelle instance d'une classe python

Je veux simuler des méthodes sur n'importe quelle instance d'une classe du code de production afin de faciliter les tests. Existe-t-il une bibliothèque dans Python qui pourrait faciliter cela?

Fondamentalement, je veux faire ce qui suit, mais en Python (le code suivant est Ruby, en utilisant la bibliothèque Mocha):

  def test_stubbing_an_instance_method_on_all_instances_of_a_class
    Product.any_instance.stubs(:name).returns('stubbed_name')
    assert_equal 'stubbed_name', SomeClassThatUsesProduct.get_new_product_name
  end

La chose importante à noter ci-dessus est que je dois la simuler au niveau de la classe, car j'ai en fait besoin de simuler des méthodes sur une instance créée par la chose que je teste.

Cas d'utilisation:

J'ai une classe QueryMaker qui appelle une méthode sur une instance de RemoteAPI. Je veux me moquer du RemoteAPI.get_data_from_remote_server méthode pour renvoyer une constante. Comment puis-je faire cela dans un test sans avoir à mettre un cas spécial dans le code RemoteAPI pour vérifier dans quel environnement il s'exécute.

Exemple de ce que je voulais en action:

# a.py
class A(object):
    def foo(self):
        return "A's foo"

# b.py
from a import A

class B(object):
    def bar(self):
        x = A()
        return x.foo()

# test.py
from a import A
from b import B

def new_foo(self):
    return "New foo"

A.foo = new_foo

y = B()
if y.bar() == "New foo":
    print "Success!"
35
Jamie Wong

Le moyen le plus simple est probablement d'utiliser une méthode de classe. Vous devriez vraiment utiliser une méthode d'instance, mais c'est difficile de les créer, alors qu'il y a une fonction intégrée qui crée une méthode de classe. Avec une méthode de classe, votre stub obtiendra une référence à la classe (plutôt qu'à l'instance) comme premier argument, mais comme c'est un stub, cela n'a probablement pas d'importance. Alors:

Product.name = classmethod(lambda cls: "stubbed_name")

Notez que la signature du lambda doit correspondre à la signature de la méthode que vous remplacez. De plus, bien sûr, puisque Python (comme Ruby) est un langage dynamique, il n'y a aucune garantie que quelqu'un ne changera pas votre méthode tronquée pour autre chose avant de mettre la main sur l'instance , bien que j'espère que vous saurez assez rapidement si cela se produit.

Edit: Après enquête, vous pouvez laisser de côté la classmethod():

Product.name = lambda self: "stubbed_name"

J'essayais de préserver le comportement de la méthode d'origine aussi près que possible, mais il semble que ce ne soit pas réellement nécessaire (et ne préserve pas le comportement comme je l'espérais, de toute façon).

3
kindall

Avoir besoin de se moquer des méthodes lors des tests est très courant et il existe de nombreux outils pour vous aider avec Python. Le danger avec des classes de "correction de singe" comme celle-ci est que si vous ne le faites pas annulez ensuite la classe a été modifiée pour toutes les autres utilisations de votre tests.

Ma bibliothèque de simulation, qui est l'une des bibliothèques de simulation les plus populaires Python, comprend un assistant appelé "patch" qui vous aide à corriger en toute sécurité des méthodes ou des attributs sur des objets et des classes pendant vos tests.

Le module de simulation est disponible à partir de:

http://pypi.python.org/pypi/mock

Le patch décorateur peut être utilisé comme gestionnaire de contexte ou comme décorateur de test. Vous pouvez soit l'utiliser pour patcher avec des fonctions vous-même, soit l'utiliser pour patcher automatiquement avec des objets Mock qui sont très configurables.

from a import A
from b import B

from mock import patch

def new_foo(self):
    return "New foo"

with patch.object(A, 'foo', new_foo):
    y = B()
    if y.bar() == "New foo":
        print "Success!"

Cela gère automatiquement le dépatching. Vous pouvez vous en sortir sans définir vous-même la fonction de simulation:

from mock import patch

with patch.object(A, 'foo') as mock_foo:
    mock_foo.return_value = "New Foo"

    y = B()
    if y.bar() == "New foo":
        print "Success!"
55
fuzzyman

La simulation est la façon de le faire, d'accord. Il peut être un peu difficile de s'assurer que vous corrigez la méthode d'instance sur toutes les instances créées à partir de la classe.

# a.py
class A(object):
    def foo(self):
        return "A's foo"

# b.py
from a import A

class B(object):
    def bar(self):
        x = A()
        return x.foo()

# test.py
from a import A
from b import B
import mock

mocked_a_class = mock.Mock()
mocked_a_instance = mocked_a_class.return_value
mocked_a_instance.foo.return_value = 'New foo'

with mock.patch('b.A', mocked_a_class):  # Note b.A not a.A
    y = B()
    if y.bar() == "New foo":
        print "Success!"

Référencé dans le docs , au para commençant "Pour configurer les valeurs de retour sur les méthodes d'instances sur la classe corrigée ..."

3
Fush

Je ne sais pas Ruby assez bien pour dire exactement ce que vous essayez de faire, mais consultez le __getattr__ méthode. Si vous le définissez dans votre classe, Python l'appellera lorsque le code essaiera d'accéder à tout attribut de votre classe qui n'est pas défini autrement. Puisque vous voulez que ce soit une méthode, il faudra pour créer une méthode à la volée qu'elle renvoie.

>>> class Product:
...     def __init__(self, number):
...         self.number = number
...     def get_number(self):
...         print "My number is %d" % self.number
...     def __getattr__(self, attr_name):   
...         return lambda:"stubbed_"+attr_name
... 
>>> p = Product(172)
>>> p.number
172
>>> p.name()
'stubbed_name'
>>> p.get_number()
My number is 172
>>> p.other_method()
'stubbed_other_method'

Notez également que __getattr__ doit ne pas utiliser aucun autre attribut non défini de votre classe, sinon il sera infiniment récursif, appelant __getattr__ pour l'attribut qui n'existe pas.

...     def __getattr__(self, attr_name):   
...         return self.x
>>> p.y
Traceback (most recent call last):
#clipped
RuntimeError: maximum recursion depth exceeded while calling a Python object

Si c'est quelque chose que vous voulez seulement faire à partir de votre code de test, pas du code de production, puis mettez votre définition de classe normale dans le fichier de code de production, puis dans le code de test définissez le __getattr__ méthode (non liée), puis liez-la à la classe souhaitée:

#production code
>>> class Product:
...     def __init__(self, number):
...         self.number = number
...     def get_number(self):
...         print "My number is %d" % self.number
...         

#test code
>>> def __getattr__(self, attr):
...     return lambda:"stubbed_"+attr_name
... 
>>> p = Product(172)
>>> p.number
172
>>> p.name()
Traceback (most recent call last):
  File "<interactive input>", line 1, in <module>
AttributeError: Product instance has no attribute 'name'
>>> Product.__getattr__ = __getattr__
>>> p.name()
'stubbed_name'

Je ne sais pas comment cela réagirait avec une classe qui utilisait déjà __getattribute__ (par opposition à __getattr__, __getattribute__ est appelé pour tous les attributs, qu'ils existent ou non).

Si vous ne souhaitez le faire que pour des méthodes spécifiques qui existent déjà, vous pouvez faire quelque chose comme:

#production code
>>> class Product:
...     def __init__(self, number):
...         self.number = number
...     def get_number(self):
...         return self.number
...     
>>> p = Product(172)
>>> p.get_number()
172

#test code
>>> def get_number(self):
...     return "stub_get_number"
... 
>>> Product.get_number = get_number
>>> p.get_number()
'stub_get_number'

Ou si vous vouliez vraiment être élégant, vous pouvez créer une fonction wrapper pour faciliter l'exécution de plusieurs méthodes:

#test code
>>> import functools
>>> def stubber(fn):
...     return functools.wraps(fn)(lambda self:"stub_"+fn.__name__)
... 
>>> Product.get_number = stubber(Product.get_number)
>>> p.get_number()
'stub_get_number'
1
user470379