Afirmar que se llamó a un método en una prueba unitaria de Python

91

Supongamos que tengo el siguiente código en una prueba unitaria de Python:

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

¿Existe una manera fácil de afirmar que aw.Clear()se llamó a un método en particular (en mi caso ) durante la segunda línea de la prueba? por ejemplo, hay algo como esto:

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

Respuestas:

150

Yo uso Mock (que ahora es unittest.mock en py3.3 +) para esto:

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)

Para su caso, podría verse así:

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 admite bastantes características útiles, incluidas las formas de parchear un objeto o módulo, así como verificar que se haya llamado lo correcto, etc.

¡Advertencia emptor! (¡El comprador tenga cuidado!)

Si escribe incorrectamente assert_called_with( assert_called_onceo assert_called_wiht), es posible que la prueba aún se ejecute, ya que Mock pensará que se trata de una función simulada y la seguirá felizmente, a menos que utilice autospec=true. Para obtener más información, lea assert_called_once: Threat or Menace .

Macke
fuente
5
+1 para iluminar discretamente mi mundo con el maravilloso módulo Mock.
Ron Cohen
@RonCohen: Sí, es bastante sorprendente y también mejora todo el tiempo. :)
Macke
1
Si bien el uso de simulacro es definitivamente el camino a seguir, desaconsejaría usar assert_called_once, simplemente no existe :)
FelixCQ
se ha eliminado en versiones posteriores. Mis pruebas todavía lo están usando. :)
Macke
1
Vale la pena repetir lo útil que es usar autospec = True para cualquier objeto simulado porque realmente puede morderlo si escribe mal el método de aserción.
rgilligan
30

Sí, si está utilizando Python 3.3+. Puede utilizar el unittest.mockmétodo integrado para afirmar llamado. Para Python 2.6+, use el backport rodante Mock, que es lo mismo.

Aquí hay un ejemplo rápido en su caso:

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

No tengo conocimiento de nada incorporado. Es bastante simple de implementar:

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()

Esto requiere que el objeto en sí no modifique self.b, lo que casi siempre es cierto.

Glenn Maynard
fuente
Dije que mi Python estaba oxidado, aunque probé mi solución para asegurarme de que funciona :-) Internalicé Python antes de la versión 2.5, de hecho nunca usé 2.5 para ningún Python significativo ya que tuvimos que congelarlo en 2.3 para compatibilidad con lib. Al revisar su solución, encontré effbot.org/zone/python-with-statement.htm como una descripción clara y agradable. Yo sugeriría humildemente que mi enfoque parece más pequeño y podría ser más fácil de aplicar si desea más de un punto de registro, en lugar de anidar "con" s. Realmente me gustaría que me explicara si hay algún beneficio particular suyo.
Andy Dent
@Andy: Su respuesta es más pequeña porque es parcial: en realidad no prueba los resultados, no restaura la función original después de la prueba para que pueda continuar usando el objeto, y tiene que escribir repetidamente el código para hacer todo eso de nuevo cada vez que escribes una prueba. La cantidad de líneas de código de soporte no es importante; esta clase va en su propio módulo de prueba, no en línea en una cadena de documentos; esto requiere una o dos líneas de código en la prueba real.
Glenn Maynard
6

Sí, puedo darte el esquema, pero mi Python está un poco oxidado y estoy demasiado ocupado para explicarlo en detalle.

Básicamente, debe colocar un proxy en el método que llamará al original, por ejemplo:

 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)

Esta respuesta de StackOverflow sobre invocables puede ayudarlo a comprender lo anterior.

Con más detalle:

Aunque la respuesta fue aceptada, debido a la interesante discusión con Glenn y al tener unos minutos libres, quería ampliar mi respuesta:

# 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
fuente
bonito. Agregué un recuento de llamadas a methCallLogger para poder afirmarlo.
Mark Heath
¿Esto sobre la solución completa e independiente que proporcioné? ¿Seriamente?
Glenn Maynard
@Glenn Soy muy nuevo en Python, tal vez el tuyo sea mejor, pero todavía no lo entiendo todo. Pasaré un poco de tiempo probándolo después.
Mark Heath
Esta es, con mucho, la respuesta más simple y fácil de entender. ¡Muy buen trabajo!
Matt Messersmith
4

Puede simularloaw.Clear , ya sea manualmente o utilizando un marco de prueba como pymox . Manualmente, lo haría usando algo como esto:

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

Usando pymox, lo haría así:

class MyTest(mox.MoxTestBase):
  def testClear():
    aw = self.m.CreateMock(aps.Request)
    aw.Clear()
    self.mox.ReplayAll()
    aps.Request('nv2', aw)
Max Shawabkeh
fuente
También me gusta este enfoque, aunque todavía quiero que llamen a old_clear. Esto hace que sea obvio lo que está pasando.
Mark Heath