Cuando y como usar Python mock
Reduzca el tiempo de ejecución de su prueba con simulacro
Akshar Raaj
19 de agosto de 2019·4 min de lectura
Agenda
Esta publicación cubrirá cuándo y cómo usar unittest.mock
Biblioteca.
Los documentos de Python describen adecuadamente la biblioteca simulada:
unittest.mock allows you to replace parts of your system under test with mock objects and make assertions about how they have been used.
Cuando usar la biblioteca simulada
La respuesta corta es «la mayoría de las veces». Los siguientes ejemplos aclararían esta afirmación:
Defina las siguientes tres funciones en shell.
In [1]: def cow():
...: print("cow")
...: return {'cow': 'moo'}
...:In [2]: def dog():
...: print("dog")
...: return {'dog': 'bark'}
...:In [3]: def animals():
...: data = cow()
...: data.update(dog())
...: data['pig'] = 'oink'
...: return data
...:
Vamos a ejecutar animals
. Los veganos me matarían;)
In [4]: animals()
cow
dog
Out[4]: {'cow': 'moo', 'dog': 'bark', 'pig': 'oink'}
La salida confirma que cow
y dog
fueron invocados desde animals()
.
Escribamos una prueba para cow
.
In [5]: def test_cow():
...: assert cow() == {'cow': 'moo'}
...:
Vamos a ejecutar test_cow
para asegurar cow
se está comportando como se esperaba.
In [6]: test_cow()
cow
Probemos de manera similar dog
.
In [7]: def test_dog():
...: assert dog() == {'dog': 'bark'}
...:In [8]: test_dog()
dog
Agreguemos una prueba para animals
.
In [9]: def test_animals():
...: assert animals() == {'dog': 'bark', 'cow': 'moo', 'pig': 'oink'}
...:In [10]: test_animals()
cow
dog
Como se desprende de las declaraciones impresas, cow()
y dog()
fueron ejecutados desde test_animals()
.
miXecuación de cow
y dog
son innecesarios durante la prueba animals
porque cow
y dog
ya se han probado de forma aislada.
Mientras prueba animals
, solo queremos estar seguros de que cow
y dog
sería ejecutado. No queremos que suceda la ejecución real.
cow
y dog
son funciones diminutas. Hay ejecución desde test_animals
no es un gran problema actualmente. Tenía funciones cow
y dog
ha sido enorme, test_animals
habría llevado mucho tiempo completarlo.
Este escenario se puede evitar si usamos unitest.mock.patch
.
Vamos a modificar test_animals
tener el siguiente aspecto:
In [17]: from unittest.mock import patchIn [18]: @patch('__main__.cow')
...: @patch('__main__.dog')
...: def test_animals(patched_dog, patched_cow):
...: data = animals()
...: assert patched_dog.called is True
...: assert patched_cow.called is True
...:
Ejecute `test_animals ()`.
In [19]: test_animals()
No podemos ver declaraciones impresas de cow
y dog
ya no. Esto confirma que cow
y dog
no fueron ejecutados.
Vamos a diseccionar test_animals
.
test_animals
ha sido decorado con @patch
. Función dog
se pasa como argumento a @patch
.
Como test_animals
ha sido decorado, por lo que en el contexto de test_animals
, función real dog
ha sido reemplazado por un unittest.mock.Mock
ejemplo. Esta Mock
instancia se está refiriendo como patched_dog
.
Ya que animals()
se ejecuta en el contexto de test_animals()
, por lo que no se realiza ninguna llamada real a dog
de animals
. En lugar de patched_dog
se llama.
Mock
las instancias tienen un atributo llamado called
que se establece en verdadero si se invoca una instancia de Mock desde una función bajo prueba. Afirmamos que la instancia simulada, patched_dog
, se ha invocado.
Si llama a dog
de animals
es eliminado / comentado, luego test_animals
fallaría.
In [20]: def animals():
...: data = cow()
...: #data.update(dog())
...: data['pig'] = 'oink'
...: return data
...:In [21]: test_animals()
---------------------------------------------------------------------------
AssertionError Traceback (most recent call last)
<ipython-input-21-f8c77986484f> in <module>
----> 1 test_animals()/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/unittest/mock.py in patched(*args, **keywargs)
1177
1178 args += tuple(extra_args)
-> 1179 return func(*args, **keywargs)
1180 except:
1181 if (patching not in entered_patchers and<ipython-input-18-a243d0eea2e8> in test_animals(patched_dog, patched_cow)
3 def test_animals(patched_dog, patched_cow):
4 data = animals()
----> 5 assert patched_dog.called is True
6 assert patched_cow.called is True
7AssertionError:
Esto confirma que la prueba proporciona una cobertura adecuada sin ejecutar realmente cow
y dog
.
Estableciendo return_value en una instancia de Mock
Descomente el código comentado.
Hay un código adicional en animals
aparte de llamar cow
y dog
. animals
agrega cerdo a los datos también.
Probemos el código adicional de animals
.
In [43]: @patch('__main__.cow')
...: @patch('__main__.dog')
...: def test_animals(patched_dog, patched_cow):
...: patched_cow.return_value = {'c': 'm'}
...: patched_dog.return_value = {'d': 'b'}
...: data = animals()
...: assert patched_dog.called is True
...: assert patched_cow.called is True
...: assert 'pig' in data
Ejecutemos la prueba
In [45]: test_animals()
Todas nuestras afirmaciones pasaron porque no había AssertionError.
El valor de retorno predeterminado de llamar a Mock
la función es otra Mock
ejemplo. Así que cuando patched_dog
se invoca desde animals
en test_animals
contexto, devuelve una instancia de Mock. No queremos que devuelva una instancia de Mock porque el código adicional de animals
espera que sea un diccionario.
Establecemos un return_value
sobre patched_cow
ser un diccionario. Lo mismo es cierto para patched_dog
.
Ahora código adicional de animals
también está cubierto por la prueba.
Otro ejemplo
Definamos una función para probar si una URL es válida. Esto se basa en Python requests
.
In [59]: import requestsIn [60]: def is_valid_url(url):
...: try:
...: response = requests.get(url)
...: except Exception:
...: return False
...: return response.status_code == 200
Agreguemos una prueba para is_valid_url
.
In [69]: def test_is_valid_url():
...: assert is_valid_url('https://agiliq.com') is True
...: assert is_valid_url('https://agiliq.com/eerwweeee') is False # We want False in 404 pages too
...: assert is_valid_url('https://aeewererr.com') is FalseIn [70]: test_is_valid_url()
Habría notado lo lenta que es su prueba mientras realiza las llamadas de red.
Reparemos esto utilizando patch
y Mock
para hacer nuestras pruebas más rápidas.
In [71]: @patch('__main__.requests')
...: def test_is_valid_url(patched_requests):
...: patched_requests.get.return_value = Mock(status_code=200)
...: assert is_valid_url('https://agiliq.com') is True
...: patched_requests.get.return_value = Mock(status_code=404)
...: assert is_valid_url('https://agiliq.com/eerwweeee') is False # We want False in 404 pages too
...: patched_requests.get = Mock(side_effect=Exception())
...: assert is_valid_url('https://aeewererr.com') is False
...:In [72]: test_is_valid_url()
Deberías haber notado el aumento de velocidad.