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?
Afirmar llamadas sucesivas a un método simulado
jonathan livni
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)
daTrue
. 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
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
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
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_calls
en 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_work
a 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 call
En lugar de usar call.some_method
. La segunda es la que llamamos assert_has_calls
en mock_work_function
en lugar de en mock_work_function.return_value
.