Le problème
L'utilisation de mock.patch
Avec autospec=True
Pour patcher une classe ne préserve pas les attributs des instances de cette classe.
Les détails
J'essaie de tester une classe Bar
qui instancie une instance de classe Foo
en tant qu'attribut d'objet Bar
appelé foo
. La méthode Bar
sous test est appelée bar
; il appelle la méthode foo
de l'instance Foo
appartenant à Bar
. En testant cela, je me moque de Foo
, car je veux seulement tester que Bar
accède au membre Foo
correct:
import unittest
from mock import patch
class Foo(object):
def __init__(self):
self.foo = 'foo'
class Bar(object):
def __init__(self):
self.foo = Foo()
def bar(self):
return self.foo.foo
class TestBar(unittest.TestCase):
@patch('foo.Foo', autospec=True)
def test_patched(self, mock_Foo):
Bar().bar()
def test_unpatched(self):
assert Bar().bar() == 'foo'
Les classes et les méthodes fonctionnent très bien (test_unpatched
Réussit), mais lorsque j'essaye de Foo dans un cas de test (testé en utilisant à la fois nosetests et pytest) en utilisant autospec=True
, Je rencontre "AttributeError: Mock object n'a pas d'attribut 'foo' "
19:39 $ nosetests -sv foo.py
test_patched (foo.TestBar) ... ERROR
test_unpatched (foo.TestBar) ... ok
======================================================================
ERROR: test_patched (foo.TestBar)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/usr/local/lib/python2.7/dist-packages/mock.py", line 1201, in patched
return func(*args, **keywargs)
File "/home/vagrant/dev/constellation/test/foo.py", line 19, in test_patched
Bar().bar()
File "/home/vagrant/dev/constellation/test/foo.py", line 14, in bar
return self.foo.foo
File "/usr/local/lib/python2.7/dist-packages/mock.py", line 658, in __getattr__
raise AttributeError("Mock object has no attribute %r" % name)
AttributeError: Mock object has no attribute 'foo'
En effet, lorsque j'imprime mock_Foo.return_value.__dict__
, Je peux voir que foo
n'est pas dans la liste des enfants ou des méthodes:
{'_mock_call_args': None,
'_mock_call_args_list': [],
'_mock_call_count': 0,
'_mock_called': False,
'_mock_children': {},
'_mock_delegate': None,
'_mock_methods': ['__class__',
'__delattr__',
'__dict__',
'__doc__',
'__format__',
'__getattribute__',
'__hash__',
'__init__',
'__module__',
'__new__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__setattr__',
'__sizeof__',
'__str__',
'__subclasshook__',
'__weakref__'],
'_mock_mock_calls': [],
'_mock_name': '()',
'_mock_new_name': '()',
'_mock_new_parent': <MagicMock name='Foo' spec='Foo' id='38485392'>,
'_mock_parent': <MagicMock name='Foo' spec='Foo' id='38485392'>,
'_mock_wraps': None,
'_spec_class': <class 'foo.Foo'>,
'_spec_set': None,
'method_calls': []}
Ma compréhension de l'autospec est que, si c'est vrai, les spécifications du correctif devraient s'appliquer de manière récursive. Puisque foo est en effet un attribut des instances de Foo, ne devrait-il pas être corrigé? Sinon, comment puis-je obtenir la maquette Foo pour préserver les attributs des instances Foo?
REMARQUE:
Ceci est un exemple trivial qui montre le problème de base. En réalité, je me moque d'un module tiers. Classe - consul.Consul
- dont j'instancie le client dans une classe wrapper Consul que j'ai. Comme je ne gère pas le module consul, je ne peux pas modifier la source en fonction de mes tests (je ne voudrais pas vraiment le faire de toute façon). Pour ce que ça vaut, consul.Consul()
renvoie un client consul, qui a un attribut kv
- une instance de consul.Consul.KV
. kv
a une méthode get
, que j'encapsule dans une méthode d'instance get_key
dans ma classe Consul. Après avoir corrigé consul.Consul
, L'appel à get échoue en raison de AttributeError: l'objet Mock n'a pas d'attribut kv.
Ressources déjà vérifiées:
http://mock.readthedocs.org/en/latest/helpers.html#autospeccinghttp://mock.readthedocs.org/en/latest/patch.html
Non, l'autospeccing ne peut pas simuler les attributs définis dans la méthode __init__
De la classe d'origine (ou dans toute autre méthode). Il ne peut que simuler les attributs statiques , tout ce qui peut être trouvé sur la classe.
Sinon, la maquette devrait créer une instance de la classe que vous avez essayé de remplacer par une maquette en premier lieu, ce qui n'est pas une bonne idée (pensez aux classes qui créent beaucoup de ressources réelles lorsqu'elles sont instanciées).
La nature récursive d'une maquette spécifiée automatiquement est alors limitée à ces attributs statiques; si foo
est un attribut de classe, l'accès à Foo().foo
renverra une maquette spécifiée automatiquement pour cet attribut. Si vous avez une classe Spam
dont l'attribut eggs
est un objet de type Ham
, alors la maquette de Spam.eggs
Sera une maquette spécifiée automatiquement du Ham
classe.
la documentation que vous lisez explicitement couvre ceci:
Un problème plus grave est qu'il est courant que les attributs d'instance soient créés dans la méthode
__init__
Et n'existent pas du tout sur la classe.autospec
ne peut pas connaître les attributs créés dynamiquement et limite l'api aux attributs visibles.
Vous devez juste définir vous-même les attributs manquants:
@patch('foo.Foo', autospec=TestFoo)
def test_patched(self, mock_Foo):
mock_Foo.return_value.foo = 'foo'
Bar().bar()
ou créez une sous-classe de votre classe Foo
à des fins de test qui ajoute l'attribut comme attribut de classe:
class TestFoo(foo.Foo):
foo = 'foo' # class attribute
@patch('foo.Foo', autospec=TestFoo)
def test_patched(self, mock_Foo):
Bar().bar()