Afirmar llamadas sucesivas a un método simulado


175

Mock tiene un método útilassert_called_with() . Sin embargo, hasta donde yo entiendo, esto solo verifica la última llamada a un método.
Si tengo un código que llama al método simulado 3 veces sucesivas, cada vez con diferentes parámetros, ¿cómo puedo afirmar estas 3 llamadas con sus parámetros específicos?

Respuestas:


179

assert_has_calls Es otro enfoque para este problema.

De los documentos:

Claim_has_calls (llamadas, any_order = False)

afirmar que se ha llamado al simulacro con las llamadas especificadas. La lista de llamadas simuladas se verifica para las llamadas.

Si any_order es False (el valor predeterminado), las llamadas deben ser secuenciales. Puede haber llamadas adicionales antes o después de las llamadas especificadas.

Si any_order es True, las llamadas pueden estar en cualquier orden, pero todas deben aparecer en simulacros de llamadas.

Ejemplo:

>>> from unittest.mock import call, Mock
>>> mock = Mock(return_value=None)
>>> mock(1)
>>> mock(2)
>>> mock(3)
>>> mock(4)
>>> calls = [call(2), call(3)]
>>> mock.assert_has_calls(calls)
>>> calls = [call(4), call(2), call(3)]
>>> mock.assert_has_calls(calls, any_order=True)

Fuente: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.assert_has_calls


9
Un poco raro que optó por añadir un nuevo tipo de "llamada" para los que también podrían haber utilizado simplemente una lista o una tupla ...
jaapz

@jaapz Subclases tuple: isinstance(mock.call(1), tuple)da True. También agregaron algunos métodos y atributos.
jpmc26

13
Las primeras versiones de Mock usaban una tupla simple, pero resulta incómodo de usar. Cada llamada de función recibe una tupla de (args, kwargs), por lo que para verificar que "foo (123)" se haya llamado correctamente, debe "afirmar mock.call_args == ((123,), {})", que es un bocado en comparación con "llamada (123)"
Jonathan Hartley

¿Qué haces cuando en cada instancia de llamada esperas un valor de retorno diferente?
CodeWithPride

2
@CodeWithPride parece más un trabajo paraside_effect
Pigueiras

108

Por lo general, no me importa el orden de las llamadas, solo que ocurrieron. En ese caso, combino assert_any_callcon una afirmación sobre call_count.

>>> import mock
>>> m = mock.Mock()
>>> m(1)
<Mock name='mock()' id='37578160'>
>>> m(2)
<Mock name='mock()' id='37578160'>
>>> m(3)
<Mock name='mock()' id='37578160'>
>>> m.assert_any_call(1)
>>> m.assert_any_call(2)
>>> m.assert_any_call(3)
>>> assert 3 == m.call_count
>>> m.assert_any_call(4)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "[python path]\lib\site-packages\mock.py", line 891, in assert_any_call
    '%s call not found' % expected_string
AssertionError: mock(4) call not found

Creo que hacerlo de esta manera es más fácil de leer y comprender que una gran lista de llamadas transferidas a un solo método.

Si le importa el orden o espera múltiples llamadas idénticas, assert_has_callspodría ser más apropiado.

Editar

Desde que publiqué esta respuesta, he repensado mi enfoque de las pruebas en general. Creo que vale la pena mencionar que si su prueba se está volviendo tan complicada, es posible que esté haciendo pruebas inapropiadamente o tenga un problema de diseño. Los simulacros están diseñados para probar la comunicación entre objetos en un diseño orientado a objetos. Si su diseño no está orientado a objeciones (como en procedimientos o procedimientos más funcionales), la simulación puede ser totalmente inapropiada. Es posible que también tenga demasiadas cosas dentro del método, o puede estar probando detalles internos que es mejor dejar sin desmontar. Desarrollé la estrategia mencionada en este método cuando mi código no estaba muy orientado a objetos, y creo que también estaba probando detalles internos que habría sido mejor dejar sin descifrar.


@ jpmc26 ¿podría elaborar más sobre su edición? ¿A qué te refieres con "mejor no desmontar"? ¿De qué otra forma probaría si se ha realizado una llamada dentro de un método
Otgw

@memo A menudo, es mejor dejar que se llame al método real. Si se rompe el otro método, podría romper la prueba, pero el valor de evitarlo es menor que el valor de tener una prueba más simple y más fácil de mantener. Los mejores momentos para burlarse son cuando la llamada externa al otro método es lo que desea probar (por lo general, esto significa que se pasa algún tipo de resultado y el código bajo prueba no devuelve un resultado) o el otro método tiene dependencias externas (base de datos, sitios web) que desea eliminar. (Técnicamente, el último caso es más un trozo, y dudaría en afirmarlo.)
jpmc26

La burla de @ jpmc26 es útil cuando desea evitar la inyección de dependencia o algún otro método de elección de estrategia de tiempo de ejecución. como mencionó, probar la lógica interna de los métodos, sin llamar a servicios externos y, lo que es más importante, sin tener en cuenta el entorno (un no no para tener un buen código do() if TEST_ENV=='prod' else dont()), se logra fácilmente burlándose de la forma que sugirió. un efecto secundario de esto es mantener pruebas por versiones (digamos cambios de código entre la API de búsqueda de Google v1 y v2, su código probará la versión 1 sin importar qué)
Daniel Dubovski

@DanielDubovski La mayoría de sus pruebas deben estar basadas en entrada / salida. Eso no siempre es posible, pero si no es posible la mayoría de las veces, probablemente tenga un problema de diseño. Cuando necesita que se devuelva algún valor que normalmente proviene de otro fragmento de código y desea cortar una dependencia, generalmente lo hará un código auxiliar. Los simulacros solo son necesarios cuando necesita verificar que se llama a alguna función de modificación de estado (probablemente sin valor de retorno). (La diferencia entre un simulacro y un apéndice es que no se confirma en una llamada con un apéndice.) El uso de simulacros donde los apéndices harán que sus pruebas sean menos mantenibles.
jpmc26

@ jpmc26 ¿no está llamando a un servicio externo un tipo de salida? por supuesto, puede refactorizar el código que construye el mensaje que se enviará y probarlo en lugar de afirmar los parámetros de llamada, pero en mi humilde opinión, es más o menos lo mismo. ¿Cómo sugeriría rediseñar las API externas de llamadas? Estoy de acuerdo en que la burla debe reducirse al mínimo, todo lo que digo es que no puede probar los datos que envía a servicios externos para asegurarse de que la lógica se comporta como se esperaba.
Daniel Dubovski el


17

Siempre tengo que mirar esto una y otra vez, así que aquí está mi respuesta.


Afirmar llamadas a métodos múltiples en diferentes objetos de la misma clase

Supongamos que tenemos una clase de trabajo pesado (que queremos burlarnos):

In [1]: class HeavyDuty(object):
   ...:     def __init__(self):
   ...:         import time
   ...:         time.sleep(2)  # <- Spends a lot of time here
   ...:     
   ...:     def do_work(self, arg1, arg2):
   ...:         print("Called with %r and %r" % (arg1, arg2))
   ...:  

Aquí hay un código que usa dos instancias de la HeavyDutyclase:

In [2]: def heavy_work():
   ...:     hd1 = HeavyDuty()
   ...:     hd1.do_work(13, 17)
   ...:     hd2 = HeavyDuty()
   ...:     hd2.do_work(23, 29)
   ...:    


Ahora, aquí hay un caso de prueba para la heavy_workfunción:

In [3]: from unittest.mock import patch, call
   ...: def test_heavy_work():
   ...:     expected_calls = [call.do_work(13, 17),call.do_work(23, 29)]
   ...:     
   ...:     with patch('__main__.HeavyDuty') as MockHeavyDuty:
   ...:         heavy_work()
   ...:         MockHeavyDuty.return_value.assert_has_calls(expected_calls)
   ...:  

Nos estamos burlando de la HeavyDutyclase con MockHeavyDuty. Para hacer valer las llamadas a métodos provenientes de cada HeavyDutyinstancia, tenemos que referirnos MockHeavyDuty.return_value.assert_has_calls, en lugar de MockHeavyDuty.assert_has_calls. Además, en la lista de expected_callstenemos que especificar qué nombre de método estamos interesados ​​en reclamar llamadas. Entonces nuestra lista está hecha de llamadas a call.do_work, en lugar de simplemente call.

El ejercicio del caso de prueba nos muestra que es exitoso:

In [4]: print(test_heavy_work())
None


Si modificamos la heavy_workfunción, la prueba falla y produce un mensaje de error útil:

In [5]: def heavy_work():
   ...:     hd1 = HeavyDuty()
   ...:     hd1.do_work(113, 117)  # <- call args are different
   ...:     hd2 = HeavyDuty()
   ...:     hd2.do_work(123, 129)  # <- call args are different
   ...:     

In [6]: print(test_heavy_work())
---------------------------------------------------------------------------
(traceback omitted for clarity)

AssertionError: Calls not found.
Expected: [call.do_work(13, 17), call.do_work(23, 29)]
Actual: [call.do_work(113, 117), call.do_work(123, 129)]


Afirmar múltiples llamadas a una función

Para contrastar con lo anterior, aquí hay un ejemplo que muestra cómo simular múltiples llamadas a una función:

In [7]: def work_function(arg1, arg2):
   ...:     print("Called with args %r and %r" % (arg1, arg2))

In [8]: from unittest.mock import patch, call
   ...: def test_work_function():
   ...:     expected_calls = [call(13, 17), call(23, 29)]    
   ...:     with patch('__main__.work_function') as mock_work_function:
   ...:         work_function(13, 17)
   ...:         work_function(23, 29)
   ...:         mock_work_function.assert_has_calls(expected_calls)
   ...:    

In [9]: print(test_work_function())
None


Hay dos diferencias principales. La primera es que, al burlarnos de una función, configuramos nuestras llamadas esperadas usando call, en lugar de usar call.some_method. La segunda es que nosotros llamamos assert_has_callsen mock_work_functionvez de sobre mock_work_function.return_value.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.