Afirmar llamadas sucesivas a un método simulado

9 minutos de lectura

avatar de usuario
jonathan livni

Mock tiene un útil assert_called_with() método. Sin embargo, según tengo entendido, esto solo verifica el ultimo llamar a un método.
Si tengo un código que llama al método simulado 3 veces seguidas, cada vez con diferentes parámetros, ¿cómo puedo afirmar estas 3 llamadas con sus parámetros específicos?

avatar de usuario
Pigueiras

assert_has_calls es otro enfoque para este problema.

De los documentos:

afirmar_tiene_llamadas (llamadas, any_order=False)

afirmar que el simulacro ha sido llamado con las llamadas especificadas. La lista de llamadas simuladas se comprueba 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 mock_calls.

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

  • Un poco extraño, eligieron agregar un nuevo tipo de “llamada” para el cual también podrían haber usado una lista o una tupla…

    – jaapz

    20 de enero de 2015 a las 10:36

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

    – jpmc26

    16 de septiembre de 2015 a las 4:50


  • Las primeras versiones de Mock usaban una tupla simple, pero resulta ser 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 “llamar (123)”

    –Jonathan Hartley

    25 de enero de 2016 a las 20:52


  • ¿Qué hace cuando en cada instancia de llamada espera un valor de retorno diferente?

    – CódigoConOrgullo

    8 de agosto de 2017 a las 19:47

  • @CodeWithPride parece más un trabajo para side_effect

    – Pigueiras

    08/08/2017 a las 23:40

avatar de usuario
jpmc26

Por lo general, no me importa el orden de las llamadas, solo que sucedieron. En ese caso, combino assert_any_call con 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

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

Si le importa el orden o espera varias llamadas idénticas, assert_has_calls podrí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é probando de manera inapropiada o que 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 objetos (como si fuera más procedimental o funcional), la simulación puede ser totalmente inapropiada. También es posible que haya demasiadas cosas dentro del método, o que esté probando detalles internos que es mejor dejar sin burlar. 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 hubiera sido mejor no modificar.

  • @ jpmc26, ¿podría dar más detalles sobre su edición? ¿Qué quieres decir con ‘mejor dejarlo sin burlar’? ¿De qué otra manera probaría si se ha realizado una llamada dentro de un método?

    – otgw

    15 de diciembre de 2015 a las 16:04

  • @memo A menudo, es mejor dejar que se llame al método real. Si el otro método no funciona, podría romper la prueba, pero el valor de evitar eso es menor que el valor de tener una prueba más simple y mantenible. Los mejores momentos para simular son cuando la llamada externa al otro método es que quieres probar (Por lo general, esto significa que se pasa algún tipo de resultado y el código bajo prueba no devuelve ningún 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

    15 de diciembre de 2015 a las 16:32

  • La simulación 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 mencionaste, probando la lógica interna de los métodos, sin llamar a servicios externos y, lo que es más importante, sin ser consciente del entorno (un no no para que un buen código tenga 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 las pruebas por versiones (por ejemplo, los 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

    27/10/2016 a las 12:43


  • @DanielDubovski La mayoría de sus pruebas deben basarse en entrada/salida. Eso no siempre es posible, pero si no es posible la mayor parte del tiempo, probablemente tenga un problema de diseño. Cuando necesita que se devuelva algún valor que normalmente proviene de otra pieza de código y desea eliminar una dependencia, generalmente lo hará con un stub. 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 stub es que usted no confirma una llamada con un stub). El uso de simulacros donde los stubs son suficientes hace que sus pruebas sean menos fáciles de mantener.

    – jpmc26

    27/10/2016 a las 17:12


  • @ jpmc26 no está llamando a un servicio externo una especie de salida? por supuesto, puede refactorizar el código que crea 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 llamadas a las API externas? 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 espera.

    -Daniel Dubovski

    30 oct 2016 a las 8:50

avatar de usuario
jonathan livni

Puedes usar el Mock.call_args_list atributo para comparar parámetros con llamadas a métodos anteriores. Que en conjunto con Mock.call_count atributo debe darle el control total.

  • assert_has_calls sólo comprueba si se han realizado las llamadas esperadas, pero no si son las únicas.

    – azulado

    1 de junio de 2016 a las 12:13

avatar de usuario
Pedro M Duarte

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

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

Supongamos que tenemos una clase de servicio pesado (que queremos simular):

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 del HeavyDuty clase:

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 el heavy_work funció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 HeavyDuty clase con MockHeavyDuty. Para afirmar llamadas a métodos provenientes de cada HeavyDuty instancia a la que nos tenemos que referir MockHeavyDuty.return_value.assert_has_callsen vez de MockHeavyDuty.assert_has_calls. Además, en la lista de expected_calls tenemos que especificar para qué nombre de método estamos interesados ​​en afirmar las llamadas. Así que nuestra lista está hecha de llamadas a call.do_worka diferencia de simplemente call.

Ejercitar el caso de prueba nos muestra que es exitoso:

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

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

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 varias 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 simular una función configuramos nuestras llamadas esperadas usando callEn lugar de usar call.some_method. La segunda es la que llamamos assert_has_calls en mock_work_functionen lugar de en mock_work_function.return_value.

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad