example - ¿Puedo parchear un decorador de Python antes de que envuelva una función?
pytest mock (7)
Tengo una función con un decorador que intento probar con la ayuda de la biblioteca de Python Mock . Me gustaría utilizar mock.patch para reemplazar el decorador real con un decorador simulacro ''bypass'' que simplemente llama a la función. Lo que no puedo entender es cómo aplicar el parche antes de que el decorador real envuelva la función. Probé algunas variaciones diferentes en el objetivo del parche y reordené el parche e importé las declaraciones, pero sin éxito. ¿Algunas ideas?
Concepto
Esto puede sys.path
un poco extraño, pero uno puede parchear sys.path
, con una copia de sí mismo, y realizar una importación dentro del alcance de la función de prueba. El siguiente código muestra el concepto.
from unittest.mock import patch
import sys
@patch(''sys.modules'', sys.modules.copy())
def testImport():
oldkeys = set(sys.modules.keys())
import MODULE
newkeys = set(sys.modules.keys())
print((newkeys)-(oldkeys))
oldkeys = set(sys.modules.keys())
testImport() -> ("MODULE") # Set contains MODULE
newkeys = set(sys.modules.keys())
print((newkeys)-(oldkeys)) -> set() # An empty set
MODULE
puede ser sustituido con el módulo que está probando. (Esto funciona en Python 3.6 con MODULE
sustituido con xml
por ejemplo)
OP
Para su caso, digamos que la función del decorador reside en el módulo pretty
y la función decorada reside en el present
, luego debe parchear pretty.decorator
utilizando la maquinaria simulada y sustituir el MODULE
con el present
. Algo como lo siguiente debería funcionar (no probado).
clase TestDecorator (unittest.TestCase): ...
@patch(`pretty.decorator`, decorator)
@patch(`sys.path`, sys.path.copy())
def testFunction(self, decorator) :
import present
...
Explicación
Esto funciona al proporcionar un sys.path
"limpio" para cada función de prueba, utilizando una copia del sys.path
actual del módulo de prueba. Esta copia se realiza cuando el módulo se analiza por primera vez, asegurando un sys.path
consistente para todas las pruebas.
Matices
Sin embargo, hay algunas implicaciones. Si el marco de prueba ejecuta múltiples módulos de prueba bajo la misma sesión de python, cualquier módulo de prueba que importe MODULE
globalmente rompe cualquier módulo de prueba que lo importe localmente. Esto obliga a realizar la importación localmente en todas partes. Si el marco ejecuta cada módulo de prueba en una sesión de Python por separado, entonces esto debería funcionar. Del mismo modo, no puede importar el MODULE
nivel mundial dentro de un módulo de prueba donde está importando el MODULE
localmente.
Las importaciones locales deben realizarse para cada función de prueba dentro de una subclase de unittest.TestCase
. Quizás sea posible aplicar esto a la subclase unittest.TestCase
directamente haciendo que una importación particular del módulo esté disponible para todas las funciones de prueba dentro de la clase.
Ins construido
Quienes entren en problemas con las importaciones builtin
encontrarán que reemplazar el MODULE
con sys
, os
, etc. fallará, ya que estos ya están en sys.path
cuando intentas copiarlo. El truco aquí es invocar Python con las importaciones integradas deshabilitadas, creo que python -X test.py
lo hará, pero olvidé la bandera apropiada (Ver python --help
). Posteriormente, pueden importarse localmente utilizando import builtins
, IIRC.
Cabe señalar que varias de las respuestas aquí parcharán el decorador para toda la sesión de prueba en lugar de una sola instancia de prueba; que puede ser indeseable. A continuación, le mostramos cómo aplicarle un parche a un decorador que solo persiste en una sola prueba.
Nuestra unidad para ser probada con el decorador no deseado:
# app/uut.py
from app.decorators import func_decor
@func_decor
def unit_to_be_tested():
# Do stuff
pass
Desde el módulo de decoradores:
# app/decorators.py
def func_decor(func):
def inner(*args, **kwargs):
print "Do stuff we don''t want in our test"
return func(*args, **kwargs)
return inner
En el momento en que nuestra prueba se recopila durante una prueba, el decorador no deseado ya se ha aplicado a nuestra unidad bajo prueba (porque eso ocurre en el momento de la importación). Para deshacernos de eso, necesitaremos reemplazar manualmente el decorador en el módulo del decorador y luego volver a importar el módulo que contiene nuestro UUT.
Nuestro módulo de prueba:
# test_uut.py
from unittest import TestCase
from app import uut # Module with our thing to test
from app import decorators # Module with the decorator we need to replace
import imp # Library to help us reload our UUT module
from mock import patch
class TestUUT(TestCase):
def setUp(self):
# Do cleanup first so it is ready if an exception is raised
def kill_patches(): # Create a cleanup callback that undoes our patches
patch.stopall() # Stops all patches started with start()
imp.reload(uut) # Reload our UUT module which restores the original decorator
self.addCleanup(kill_patches) # We want to make sure this is run so we do this in addCleanup instead of tearDown
# Now patch the decorator where the decorator is being imported from
patch(''app.decorators.func_decor'', lambda x: x).start() # The lambda makes our decorator into a pass-thru. Also, don''t forget to call start()
# HINT: if you''re patching a decor with params use something like:
# lambda *x, **y: lambda f: f
imp.reload(uut) # Reloads the uut.py module which applies our patched decorator
La devolución de llamada de limpieza, kill_patches, restaura el decorador original y lo vuelve a aplicar a la unidad que estábamos probando. De esta forma, nuestro parche solo persiste a través de una única prueba en lugar de toda la sesión, que es exactamente como se debe comportar cualquier otro parche. Además, desde las llamadas de limpieza patch.stopall (), podemos iniciar cualquier otro parche en el setUp () que necesitamos y se limpiarán en un solo lugar.
Lo importante de entender sobre este método es cómo la recarga afectará las cosas. Si un módulo tarda demasiado o tiene una lógica que se ejecuta en la importación, puede que necesite encogerse de hombros y probar el decorador como parte de la unidad. :( Esperemos que su código esté mejor escrito que eso. ¿Verdad?
Si a uno no le importa si el parche se aplica a toda la sesión de prueba , la forma más fácil de hacerlo es en la parte superior del archivo de prueba:
# test_uut.py
from mock import patch
patch(''app.decorators.func_decor'', lambda x: x).start() # MUST BE BEFORE THE UUT GETS IMPORTED ANYWHERE!
from app import uut
Asegúrese de parchear el archivo con el decorador en lugar del alcance local del UUT y de iniciar el parche antes de importar la unidad con el decorador.
Curiosamente, incluso si el parche se detiene, todos los archivos que ya se importaron seguirán teniendo el parche aplicado al decorador, que es el reverso de la situación con la que comenzamos. Tenga en cuenta que este método aplicará parches a otros archivos en la ejecución de prueba que se importen posteriormente, incluso si no declaran un parche ellos mismos.
Cuando me topé por primera vez con este problema, solía atormentar mi cerebro durante horas. Encontré una manera mucho más fácil de manejar esto.
Esto omitirá por completo al decorador, ya que el objetivo ni siquiera estaba decorado en primer lugar.
Esto se divide en dos partes. Sugiero leer el siguiente artículo.
http://alexmarandon.com/articles/python_mock_gotchas/
Dos Gotchas con las que me encontré
1.) Simula el decorador antes de la importación de su función / módulo.
Los decoradores y las funciones se definen en el momento en que se carga el módulo. Si no se burla antes de importar, ignorará el simulacro. Después de la carga, tienes que hacer un extraño mock.patch.object, que se vuelve aún más frustrante.
2.) Asegúrate de burlarte de la ruta correcta hacia el decorador.
Recuerda que el parche del decorador que te estás burlando se basa en cómo carga tu módulo el decorador, no en cómo carga tu prueba el decorador. Es por eso que sugiero usar siempre rutas completas para las importaciones. Esto hace que las cosas sean mucho más fáciles de probar.
Pasos:
1.) La función simulada:
from functools import wraps
def mock_decorator(*args, **kwargs):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
return f(*args, **kwargs)
return decorated_function
return decorator
2.) Burlarse del decorador:
2a.) Camino dentro con.
with mock.patch(''path.to.my.decorator'', mock_decorator):
from mymodule import myfunction
2b.) Parche en la parte superior del archivo, o en TestCase.setUp
mock.patch(''path.to.my.decorator'', mock_decorator).start()
Cualquiera de estas formas le permitirá importar su función en cualquier momento dentro de la TestCase o su método / casos de prueba.
from mymodule import myfunction
2.) Use una función separada como efecto secundario de mock.patch.
Ahora puedes usar mock_decorator para cada decorador que quieras burlar. Tendrá que burlarse de cada decorador por separado, así que tenga cuidado con los que extrañe.
Lo siguiente funcionó para mí:
- Elimine la instrucción de importación que carga el objetivo de prueba.
- Parchee el decorador al inicio de la prueba como se aplicó anteriormente.
- Invoque importlib.import_module () inmediatamente después de parchear para cargar el objetivo de prueba.
- Ejecuta pruebas normalmente
Funcionó a las mil maravillas.
Los decoradores se aplican al tiempo de definición de función. Para la mayoría de las funciones, esto es cuando el módulo está cargado. (Las funciones que se definen en otras funciones tienen el decorador aplicado cada vez que se llama a la función envolvente).
Entonces, si quieres ponerle un parche a un decorador, lo que debes hacer es:
- Importar el módulo que lo contiene
- Definir la función de decorador simulado
- Establecer, por ejemplo,
module.decorator = mymockdecorator
- Importe los módulos que usan el decorador, o úselos en su propio módulo
Si el módulo que contiene el decorador también contiene funciones que lo utilizan, esos ya están decorados para cuando los pueda ver, y usted probablemente sea SOL
Edite para reflejar los cambios en Python desde que escribí esto originalmente: si el decorador usa functools.wraps()
y la versión de Python es lo suficientemente nueva, puede desenterrar la función original usando el __wrapped__
__wrapped__ y volver a decorarla, pero esto de ninguna manera está garantizado, y el decorador que desea reemplazar también puede no ser el único decorador aplicado.
Tal vez puedas aplicar otro decorador a las definiciones de todos tus decoradores, que básicamente verifican algunas variables de configuración para ver si el modo de prueba está destinado a ser utilizado.
Si es así, reemplaza al decorador que está decorando con un decorador simulado que no hace nada.
De lo contrario, deja pasar a este decorador.
para @lru_cache (max_size = 1000)
cache.LruCache = MockedLruCache
class MockedLruCache(object):
def __init__(self, maxsize=0, timeout=0):
pass
def __call__(self, func):
return func
si usa el decorador que no tiene params, debe:
from tornado import web
web.authenticated = MockAuthenticated
def MockAuthenticated(func):
return func