How to Run Unit Tests in Python Without Testing Your Patience
La mayoría de las veces, el software que escribimos interactúa directamente con lo que etiquetaríamos como servicios «sucios». En términos sencillos: servicios que son cruciales para nuestra aplicación, pero cuyas interacciones tienen efectos secundarios deseados pero no deseados, es decir, no deseados en el contexto de una ejecución de prueba autónoma.,Facebook Facebook por ejemplo: tal vez estamos escribiendo una aplicación social y queremos probar nuestra nueva función ‘Publicar en Facebook’, pero no queremos publicar en Facebook cada vez que ejecutamos nuestro conjunto de pruebas.
La Biblioteca Python unittest
incluye un subpaquete llamado unittest.mock
—o si lo declara como una dependencia, simplemente mock
—que proporciona medios extremadamente poderosos y útiles para burlarse y eliminar estos efectos secundarios no deseados.,
Nota: mock
se incluye recientemente en la biblioteca estándar a partir de Python 3.3; las distribuciones anteriores tendrán que usar la biblioteca falsa descargable a través de PyPI.
llamadas al sistema vs.burlarse de Python
para darle otro ejemplo, y uno que ejecutaremos para el resto del artículo, considere las llamadas al sistema., No es difícil ver que estos son candidatos principales para burlarse: ya sea que esté escribiendo un script para expulsar una unidad de CD, un servidor web que elimina archivos de caché anticuados de /tmp
, o un servidor de socket que se une a un puerto TCP, todas estas llamadas tienen efectos secundarios no deseados en el contexto de sus pruebas unitarias.
como desarrollador, le importa más que su biblioteca llame con éxito a la función del sistema para expulsar un CD (con los argumentos correctos, etc.).) a diferencia de experimentar realmente su bandeja de CD abierta cada vez que se ejecuta una prueba. (O peor aún, varias veces, ya que varias pruebas hacen referencia al código de expulsión durante una sola prueba unitaria!)
del mismo modo, mantener sus pruebas unitarias eficientes y de rendimiento significa mantener la mayor cantidad de «código lento» fuera de las ejecuciones de prueba automatizadas, es decir, el sistema de archivos y el acceso a la red.,
para nuestro primer ejemplo, refactorizaremos un caso de prueba estándar de Python del formulario original a uno usando mock
. Demostraremos cómo escribir un caso de prueba con simks hará que nuestras pruebas sean más inteligentes, rápidas y capaces de revelar más sobre cómo funciona el software.
una función de eliminación simple
todos necesitamos eliminar archivos de nuestro sistema de archivos de vez en cuando, así que escribamos una función en Python que hará que sea un poco más fácil para nuestros scripts hacerlo.,
#!/usr/bin/env python# -*- coding: utf-8 -*-import osdef rm(filename): os.remove(filename)
obviamente, nuestro método rm
en este momento no proporciona mucho más que el método subyacente os.remove
, pero nuestra base de código mejorará, lo que nos permitirá agregar más funcionalidad aquí.
vamos a escribir un caso de prueba tradicional, es decir, sin burlas:
nuestro caso de prueba es bastante simple, pero cada vez que se ejecuta, se crea un archivo temporal y luego se elimina., Además, no tenemos forma de probar si nuestro método rm
pasa correctamente el argumento a la llamada os.remove
. Podemos suponer que lo hace basado en la prueba anterior, pero mucho queda por desear.
refactorización con Python se burla
vamos a refactorizar nuestro caso de prueba usando mock
:
con estos refactores, hemos cambiado fundamentalmente la forma en que funciona la prueba. Ahora, tenemos un insider, un objeto que podemos usar para verificar la funcionalidad de otro.,
posibles trampas para burlarse de Python
Una de las primeras cosas que debería sobresalir es que estamos usando el decorador de métodomock.patch
para burlarse de un objeto ubicado enmymodule.os
, e inyectando ese simulacro en nuestro método de caso de prueba. ¿No tendría más sentido simplemente burlarse de os
en sí mismo, en lugar de la referencia a él en mymodule.os
?
bueno, Python es algo así como una serpiente furtiva cuando se trata de importar y administrar módulos., En tiempo de ejecución, el módulo mymodule
tiene su propio os
que se importa a su propio ámbito local en el módulo. Por lo tanto, si simulamos os
, no veremos los efectos del simulacro en el módulo mymodule
.
el mantra a seguir repitiendo es este:
simula un elemento donde se usa, no de donde viene.,
Si necesita simular el módulo tempfile
para myproject.app.MyElaborateClass
, probablemente necesite aplicar el mock a myproject.app.tempfile
, ya que cada módulo mantiene sus propias importaciones.
con esa trampa fuera del camino, sigamos burlándonos.
agregar validación a’rm’
el método rm
definido anteriormente es bastante simplificado. Nos gustaría que validara que existe una ruta y es un archivo antes de intentar eliminarlo a ciegas., Vamos a refactorizar rm
ser un poco más inteligente:
#!/usr/bin/env python# -*- coding: utf-8 -*-import osimport os.pathdef rm(filename): if os.path.isfile(filename): os.remove(filename)
Grandes. Ahora, ajustemos nuestro caso de prueba para mantener la cobertura.
nuestro paradigma de pruebas ha cambiado por completo. Ahora podemos verificar y validar la funcionalidad interna de los métodos sin ningún efecto secundario.
File-Removal as a Service with Mock Patch
hasta ahora, solo hemos estado trabajando con el suministro de simuladores para funciones, pero no para métodos en objetos o casos en los que la simulación es necesaria para enviar parámetros. Cubramos primero los métodos de objetos.,
comenzaremos con un refactor del método rm
en una clase de servicio. Realmente no hay una necesidad justificable, per se, de encapsular una función tan simple en un objeto, pero al menos nos ayudará a demostrar conceptos clave en mock
. Vamos a refactorizar:
notarás que no ha cambiado mucho en nuestro caso de prueba:
genial, así que ahora sabemos que RemovalService
funciona según lo planeado., Vamos a crear otro servicio que lo declare como una dependencia:
dado que ya tenemos cobertura de prueba en el método RemovalService
, no vamos a validar la funcionalidad interna del método rm
en nuestras pruebas de UploadService
. Más bien, simplemente probaremos (sin efectos secundarios, por supuesto) que UploadService
llama al método RemovalService.rm
, que sabemos «just works™» de nuestro caso de prueba anterior.
Hay dos maneras de hacer esto:
- Mock out the
RemovalService.rm
method itself., - proporcione una instancia simulada en el constructor de
UploadService
.
como ambos métodos son a menudo importantes en las pruebas unitarias, revisaremos ambos.
Opción 1: Métodos de instancia de burla
la bibliotecamock
tiene un decorador de métodos especial para burlar métodos y propiedades de instancia de objeto, el @mock.patch.object
decorador:
¡genial! Hemos validado que UploadService
llama con éxito al método rm
de nuestra instancia. ¿Notas algo interesante ahí?, El mecanismo de parcheo reemplazó el método rm
de todas las instancias RemovalService
en nuestro método de prueba. Eso significa que podemos inspeccionar las instancias por sí mismas. Si desea ver más, intente colocar un punto de interrupción en su código de burla para obtener una buena idea de cómo funciona el mecanismo de parches.
Mock Patch Pitfall: Decorator Order
cuando se utilizan varios decoradores en sus métodos de prueba, el orden es importante, y es un poco confuso. Básicamente, al asignar los decoradores a los parámetros del método, trabaje hacia atrás., Considere este ejemplo:
observe cómo nuestros parámetros se corresponden con el orden inverso de los decoradores? Eso es en parte debido a la forma en que funciona Python. Con varios decoradores de métodos, Este es el orden de ejecución en pseudocódigo:
patch_sys(patch_os(patch_os_path(test_something)))
dado que el parche a sys
es el parche más externo, se ejecutará en último lugar, lo que lo convierte en el último parámetro en los argumentos del método de prueba real. Tome nota de esto bien y use un depurador cuando ejecute sus pruebas para asegurarse de que los parámetros correctos se están inyectando en el orden correcto.,
Opción 2: crear instancias simuladas
en lugar de burlarse del método de instancia específico, podríamos suministrar una instancia Burlada a UploadService
con su constructor. Prefiero la opción 1 anterior, ya que es mucho más precisa, pero hay muchos casos en los que la opción 2 podría ser eficiente o necesaria. Vamos a refactorizar nuestra prueba de nuevo:
en este ejemplo, ni siquiera hemos tenido que parchear ninguna funcionalidad, simplemente creamos una especificación automática para la clase RemovalService
, y luego inyectamos esta instancia en nuestra UploadService
para validar la funcionalidad.,
el método mock.create_autospec
crea una instancia funcionalmente equivalente a la clase proporcionada. Lo que esto significa, en términos prácticos, es que cuando se interactúa con la instancia devuelta, generará excepciones si se usa de manera ilegal. Más específicamente, si se llama a un método con el número incorrecto de argumentos, se producirá una excepción. Esto es extremadamente importante a medida que ocurren los refactores. A medida que una biblioteca cambia, las pruebas se rompen y eso se espera. Sin usar una especificación automática, nuestras pruebas seguirán pasando a pesar de que la implementación subyacente está rota.,
Escollo: El mock.Mock
y mock.MagicMock
Clases
El mock
biblioteca también incluye dos clases importantes sobre los que la mayoría de los internos de la funcionalidad se basa en: mock.Mock
y mock.MagicMock
. Cuando se le da la opción de usar una instancia mock.Mock
, una instancia mock.MagicMock
, o una auto-spec, siempre favorezca el uso de una auto-spec, ya que ayuda a mantener sus pruebas sanas para cambios futuros., Esto se debe a que mock.Mock
y mock.MagicMock
aceptan todas las llamadas a métodos y asignaciones de propiedades independientemente de la API subyacente. Considere el siguiente caso de uso:
class Target(object): def apply(value): return valuedef method(target, value): return target.apply(value)
podemos probar esto con una instancia mock.Mock
como esta:
esta lógica parece sana, pero Modifiquemos el método Target.apply
para tomar más parámetros:
class Target(object): def apply(value, are_you_sure): if are_you_sure: return value else: return None
vuelva a ejecutar su prueba, y encontrará que todavía pasa. Esto se debe a que no está construido contra su API real., Es por eso que siempre debe usar el método create_autospec
y el parámetro autospec
con los decoradores @patch
y @patch.object
.facebook facebook API Call
ejemplo de simulación de Python: burlarse de una llamada a la API de Facebook
para terminar, vamos a escribir un ejemplo de simulación de python más aplicable en el mundo real, uno que mencionamos en la introducción: publicar un mensaje en Facebook. Escribiremos una buena clase wrapper y un caso de prueba correspondiente.,
Aquí está nuestro caso de prueba, que comprueba que publicamos el mensaje sin publicar realmente el mensaje:
como hemos visto hasta ahora, es realmente simple comenzar a escribir pruebas más inteligentes con mock
en Python.
conclusión
La biblioteca de Python mock
, si es un poco confusa para trabajar, es un cambio de juego para las pruebas unitarias. Hemos demostrado casos de uso comunes para comenzar a usar mock
en pruebas unitarias, y esperamos que este artículo ayude a los desarrolladores de Python a superar los obstáculos iniciales y escribir código excelente y probado.,