2010-09-30 11 views
46

Supongamos que tengo el siguiente código en una prueba de unidad de Python:Afirmar que un método fue llamado en una prueba de unidad Python

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

¿Hay una manera fácil de afirmar que un método particular (en mi caso aw.Clear()) fue llamado durante la segunda línea de la prueba? p.ej. ¿hay algo como esto:

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

Respuesta

80

utilizo Mock 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 tener este aspecto:

import mock 

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 es compatible con un buen número de características útiles, incluyendo formas de parchear un objeto o módulo , así como comprobar que se llamó a la cosa correcta, etc. etc.

Caveat emptor! (comprador tenga cuidado!)

Si escribe mal assert_called_with (a assert_called_once o assert_called_wiht) la prueba todavía puede funcionar, como Mock se piensa que esto es una función burlado y felizmente ir a lo largo, a menos que utilice autospec=true. Para obtener más información, lea assert_called_once: Threat or Menace.

+3

+1 para discretamente iluminar mi mundo con el maravilloso módulo Mock. –

+0

@RonCohen: Sí, es bastante sorprendente y también mejora todo el tiempo. :) – Macke

+1

Si bien el uso de simulacros es definitivamente el camino a seguir, desaconsejaría usar assert_called_once, simplemente no existe :) – FelixCQ

4

Puede burlarse cabo aw.Clear, ya sea manualmente o utilizando un marco de pruebas como pymox. De forma manual, 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, usted 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) 
+0

me gusta este enfoque también, aunque todavía quiero old_clear a ser llamado. Esto hace que sea obvio lo que está pasando. –

7

Sí, se puede dar el contorno pero mi Python es una un poco oxidado y estoy demasiado ocupado para explicarlo en detalle.

Básicamente, es necesario poner un proxy en el método que va a llamar a la 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) 

Este StackOverflow answer sobre exigible puede ayudarle a entender lo anterior.

En más detalle:

Aunque la respuesta fue aceptada, debido a la interesante discusión con Glenn y 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) 
+0

agradable. He agregado un conteo de llamadas a methCallLogger para que pueda afirmarlo. –

+0

¿Esto sobre la solución exhaustiva e independiente que proporcioné? ¿Seriamente? –

+0

@Glenn Soy muy nuevo en Python, tal vez tu sea mejor, pero aún no lo entiendo todo. Pasaré un tiempo más tarde probándolo. –

13

I No estoy enterado de nada incorporado. Es bastante fácil 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í mismo no modificará self.b, que casi siempre es cierto.

+0

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 Python significativo ya que tuvimos que congelar a 2.3 para la compatibilidad de lib. Al revisar su solución, encontré http://effbot.org/zone/python-with-statement.htm como una buena descripción clara. Sugeriría humildemente que mi enfoque parezca más pequeño y podría ser más fácil de aplicar si quisiera más de un punto de registro, en lugar de anidar "con". Realmente me gustaría que me explicaras si hay algún beneficio particular tuyo. –

+0

@Andy: Su respuesta es menor 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 el código repetidamente para hacerlo 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 docstring; esto requiere una o dos líneas de código en la prueba real. –

+0

+1 por usar un administrador de contexto, idea clara! –

10

Sí, si está utilizando Python 3.3+. Puede usar el built-in unittest.mock para afirmar el método llamado. Para Python 2.6+, use el backport rolling Mock, que es lo mismo.

Aquí está 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 
Cuestiones relacionadas