Affirmer qu'une méthode a été appelée dans un test unitaire Python

91

Supposons que j'ai le code suivant dans un test unitaire Python:

aw = aps.Request("nv1")
aw2 = aps.Request("nv2", aw)

Existe-t-il un moyen simple d'affirmer qu'une méthode particulière (dans mon cas aw.Clear()) a été appelée au cours de la deuxième ligne du test? par exemple, y a-t-il quelque chose comme ça:

#pseudocode:
assertMethodIsCalled(aw.Clear, lambda: aps.Request("nv2", aw))
Mark Heath
la source

Réponses:

150

J'utilise Mock (qui est maintenant unittest.mock sur py3.3 +) pour cela:

from mock import patch
from PyQt4 import Qt


@patch.object(Qt.QMessageBox, 'aboutQt')
def testShowAboutQt(self, mock):
    self.win.actionAboutQt.trigger()
    self.assertTrue(mock.called)

Pour votre cas, cela pourrait ressembler à ceci:

import mock
from mock import patch


def testClearWasCalled(self):
   aw = aps.Request("nv1")
   with patch.object(aw, 'Clear') as mock:
       aw2 = aps.Request("nv2", aw)

   mock.assert_called_with(42) # or mock.assert_called_once_with(42)

Mock prend en charge de nombreuses fonctionnalités utiles, notamment des moyens de patcher un objet ou un module, ainsi que la vérification que la bonne chose a été appelée, etc.

Caveat emptor! (Attention l'acheteur!)

Si vous vous trompez de saisie assert_called_withassert_called_onceou assert_called_wiht), votre test peut toujours s'exécuter, car Mock pensera qu'il s'agit d'une fonction simulée et suivra avec plaisir, à moins que vous n'utilisiez autospec=true. Pour plus d'informations, lisez assert_called_once: Threat or Menace .

Macke
la source
5
+1 pour éclairer discrètement mon monde avec le merveilleux module Mock.
Ron Cohen
@RonCohen: Ouais, c'est assez incroyable et ça s'améliore tout le temps aussi. :)
Macke
1
Bien que l'utilisation de mock soit définitivement la voie à suivre, je vous déconseille d'utiliser assert_called_once, avec n'existe tout simplement pas :)
FelixCQ
il a été supprimé dans les versions ultérieures. Mes tests l'utilisent toujours. :)
Macke
1
Cela vaut la peine de répéter à quel point il est utile d'utiliser autospec = True pour tout objet simulé, car cela peut vraiment vous mordre si vous avez mal orthographié la méthode assert.
rgilligan
30

Oui si vous utilisez Python 3.3+. Vous pouvez utiliser la unittest.mockméthode intégrée pour assert appelée. Pour Python 2.6+, utilisez le rollback backport Mock, qui est la même chose.

Voici un exemple rapide dans votre cas:

from unittest.mock import MagicMock
aw = aps.Request("nv1")
aw.Clear = MagicMock()
aw2 = aps.Request("nv2", aw)
assert aw.Clear.called
Devy
la source
14

Je ne connais rien de intégré. C'est assez simple à mettre en œuvre:

class assertMethodIsCalled(object):
    def __init__(self, obj, method):
        self.obj = obj
        self.method = method

    def called(self, *args, **kwargs):
        self.method_called = True
        self.orig_method(*args, **kwargs)

    def __enter__(self):
        self.orig_method = getattr(self.obj, self.method)
        setattr(self.obj, self.method, self.called)
        self.method_called = False

    def __exit__(self, exc_type, exc_value, traceback):
        assert getattr(self.obj, self.method) == self.called,
            "method %s was modified during assertMethodIsCalled" % self.method

        setattr(self.obj, self.method, self.orig_method)

        # If an exception was thrown within the block, we've already failed.
        if traceback is None:
            assert self.method_called,
                "method %s of %s was not called" % (self.method, self.obj)

class test(object):
    def a(self):
        print "test"
    def b(self):
        self.a()

obj = test()
with assertMethodIsCalled(obj, "a"):
    obj.b()

Cela nécessite que l'objet lui-même ne modifie pas self.b, ce qui est presque toujours vrai.

Glenn Maynard
la source
J'ai dit que mon Python était rouillé, même si j'ai testé ma solution pour m'assurer qu'elle fonctionne :-) J'ai internalisé Python avant la version 2.5, en fait je n'ai jamais utilisé 2.5 pour un Python significatif car nous avons dû geler à 2.3 pour la compatibilité de la lib. En examinant votre solution, j'ai trouvé effbot.org/zone/python-with-statement.htm comme une belle description claire. Je suggérerais humblement que mon approche semble plus petite et pourrait être plus facile à appliquer si vous vouliez plus d'un point de journalisation, plutôt que d'imbriquer des "avec". J'aimerais vraiment que vous m'expliquiez s'il y a des avantages particuliers à vous.
Andy Dent
@Andy: Votre réponse est plus petite car elle est partielle: elle ne teste pas réellement les résultats, elle ne restaure pas la fonction d'origine après le test afin que vous puissiez continuer à utiliser l'objet, et vous devez écrire le code à plusieurs reprises pour tout faire encore une fois à chaque fois que vous écrivez un test. Le nombre de lignes de code de support n'est pas important; cette classe va dans son propre module de test, pas en ligne dans une docstring - cela prend une ou deux lignes de code dans le test réel.
Glenn Maynard
6

Oui, je peux vous donner le contour mais mon Python est un peu rouillé et je suis trop occupé pour l'expliquer en détail.

Fondamentalement, vous devez mettre un proxy dans la méthode qui appellera l'original, par exemple:

 class fred(object):
   def blog(self):
     print "We Blog"


 class methCallLogger(object):
   def __init__(self, meth):
     self.meth = meth

   def __call__(self, code=None):
     self.meth()
     # would also log the fact that it invoked the method

 #example
 f = fred()
 f.blog = methCallLogger(f.blog)

Cette réponse StackOverflow sur Callable peut vous aider à comprendre ce qui précède.

Plus en détail:

Bien que la réponse ait été acceptée, en raison de la discussion intéressante avec Glenn et du fait de disposer de quelques minutes de libre, je voulais développer ma réponse:

# helper class defined elsewhere
class methCallLogger(object):
   def __init__(self, meth):
     self.meth = meth
     self.was_called = False

   def __call__(self, code=None):
     self.meth()
     self.was_called = True

#example
class fred(object):
   def blog(self):
     print "We Blog"

f = fred()
g = fred()
f.blog = methCallLogger(f.blog)
g.blog = methCallLogger(g.blog)
f.blog()
assert(f.blog.was_called)
assert(not g.blog.was_called)
Andy Dent
la source
agréable. J'ai ajouté un nombre d'appels à methCallLogger afin que je puisse m'affirmer dessus.
Mark Heath
Ceci sur la solution complète et autonome que j'ai fournie? Sérieusement?
Glenn Maynard
@Glenn Je suis très nouveau en Python - peut-être que le vôtre est meilleur - je ne comprends tout simplement pas encore tout cela. Je passerai un peu de temps plus tard à l'essayer.
Mark Heath
C'est de loin la réponse la plus simple et la plus facile à comprendre. Vraiment beau travail!
Matt Messersmith
4

Vous pouvez vous moquer aw.Clear, soit manuellement, soit en utilisant un cadre de test comme pymox . Manuellement, vous le feriez en utilisant quelque chose comme ceci:

class MyTest(TestCase):
  def testClear():
    old_clear = aw.Clear
    clear_calls = 0
    aw.Clear = lambda: clear_calls += 1
    aps.Request('nv2', aw)
    assert clear_calls == 1
    aw.Clear = old_clear

En utilisant pymox, vous le feriez comme ceci:

class MyTest(mox.MoxTestBase):
  def testClear():
    aw = self.m.CreateMock(aps.Request)
    aw.Clear()
    self.mox.ReplayAll()
    aps.Request('nv2', aw)
Max Shawabkeh
la source
J'aime aussi cette approche, même si je veux toujours que old_clear soit appelé. Cela rend évident ce qui se passe.
Mark Heath