web-dev-qa-db-fra.com

patcher une classe génère "AttributeError: l'objet Mock n'a pas d'attribut" lors de l'accès aux attributs d'instance

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

33
Clandestine

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()
36
Martijn Pieters