python - programacion - tutor de programación
En Python, ¿hay una buena expresión idiomática para usar gestores de contexto en setup/desmontaje? (5)
Estoy descubriendo que estoy usando muchos gestores de contexto en Python. Sin embargo, he estado probando varias cosas usándolos, y a menudo necesito lo siguiente:
class MyTestCase(unittest.TestCase):
def testFirstThing(self):
with GetResource() as resource:
u = UnderTest(resource)
u.doStuff()
self.assertEqual(u.getSomething(), ''a value'')
def testSecondThing(self):
with GetResource() as resource:
u = UnderTest(resource)
u.doOtherStuff()
self.assertEqual(u.getSomething(), ''a value'')
Cuando esto llega a muchas pruebas, esto va a ser aburrido, así que en el espíritu de SPOT / DRY (punto de verdad único / no te repitas), me gustaría refactorizar esos bits en la prueba setUp()
y tearDown()
métodos.
Sin embargo, tratar de hacer eso ha llevado a esta fealdad:
def setUp(self):
self._resource = GetSlot()
self._resource.__enter__()
def tearDown(self):
self._resource.__exit__(None, None, None)
Debe haber una mejor manera de hacer esto. Idealmente, en el setUp()
/ tearDown()
sin bits repetitivos para cada método de prueba (puedo ver cómo repetir un decorador en cada método podría hacerlo).
Editar: Considere que el objeto inferior es interno, y el objeto GetResource
es una cosa de terceros (que no estamos cambiando).
He cambiado el nombre de GetSlot
a GetResource
aquí; esto es más general que el caso específico, donde los administradores de contexto son la forma en que el objeto está destinado a entrar y salir de un estado bloqueado.
¿Qué hay sobre anular unittest.TestCase.run()
como se ilustra a continuación? Este enfoque no requiere llamar a ningún método privado o hacer algo para cada método, que es lo que el interlocutor quería.
from contextlib import contextmanager
import unittest
@contextmanager
def resource_manager():
yield ''foo''
class MyTest(unittest.TestCase):
def run(self, result=None):
with resource_manager() as resource:
self.resource = resource
super(MyTest, self).run(result)
def test(self):
self.assertEqual(''foo'', self.resource)
unittest.main()
Este enfoque también permite pasar la instancia de TestCase
al administrador de contexto, si desea modificar la instancia de TestCase
allí.
Argumentaría que debería separar su prueba del administrador de contexto de su prueba de la clase Slot. Incluso podría usar un objeto simulado que simule la interfaz de inicialización / finalización de la ranura para probar el objeto administrador de contexto, y luego probar su objeto de ranura por separado.
from unittest import TestCase, main
class MockSlot(object):
initialized = False
ok_called = False
error_called = False
def initialize(self):
self.initialized = True
def finalize_ok(self):
self.ok_called = True
def finalize_error(self):
self.error_called = True
class GetSlot(object):
def __init__(self, slot_factory=MockSlot):
self.slot_factory = slot_factory
def __enter__(self):
s = self.s = self.slot_factory()
s.initialize()
return s
def __exit__(self, type, value, traceback):
if type is None:
self.s.finalize_ok()
else:
self.s.finalize_error()
class TestContextManager(TestCase):
def test_getslot_calls_initialize(self):
g = GetSlot()
with g as slot:
pass
self.assertTrue(g.s.initialized)
def test_getslot_calls_finalize_ok_if_operation_successful(self):
g = GetSlot()
with g as slot:
pass
self.assertTrue(g.s.ok_called)
def test_getslot_calls_finalize_error_if_operation_unsuccessful(self):
g = GetSlot()
try:
with g as slot:
raise ValueError
except:
pass
self.assertTrue(g.s.error_called)
if __name__ == "__main__":
main()
Esto hace que el código sea más simple, evita la mezcla de preocupaciones y le permite reutilizar el administrador de contexto sin tener que codificarlo en muchos lugares.
El problema de llamar a __enter__
y __exit__
como lo hiciste, no es que lo hayas hecho: se pueden llamar fuera de una sentencia with
. El problema es que su código no tiene ninguna disposición para llamar correctamente al método __exit__
del objeto si se produce una excepción.
Entonces, la manera de hacerlo es tener un decorador que envuelva la llamada a su método original en una declaración with
. Una metaclase corta puede aplicar el decorador de forma transparente a todos los métodos denominados test * en la clase:
# -*- coding: utf-8 -*-
from functools import wraps
import unittest
def setup_context(method):
# the ''wraps'' decorator preserves the original function name
# otherwise unittest would not call it, as its name
# would not start with ''test''
@wraps(method)
def test_wrapper(self, *args, **kw):
with GetSlot() as slot:
self._slot = slot
result = method(self, *args, **kw)
delattr(self, "_slot")
return result
return test_wrapper
class MetaContext(type):
def __new__(mcs, name, bases, dct):
for key, value in dct.items():
if key.startswith("test"):
dct[key] = setup_context(value)
return type.__new__(mcs, name, bases, dct)
class GetSlot(object):
def __enter__(self):
return self
def __exit__(self, *args, **kw):
print "exiting object"
def doStuff(self):
print "doing stuff"
def doOtherStuff(self):
raise ValueError
def getSomething(self):
return "a value"
def UnderTest(*args):
return args[0]
class MyTestCase(unittest.TestCase):
__metaclass__ = MetaContext
def testFirstThing(self):
u = UnderTest(self._slot)
u.doStuff()
self.assertEqual(u.getSomething(), ''a value'')
def testSecondThing(self):
u = UnderTest(self._slot)
u.doOtherStuff()
self.assertEqual(u.getSomething(), ''a value'')
unittest.main()
(También incluí implementaciones simuladas de "GetSlot" y los métodos y funciones en su ejemplo para que yo mismo pudiera probar el decorador y la metaclase que sugiero en esta respuesta)
La manipulación de los administradores de contexto en situaciones en las que no se desea una declaración with
para limpiar las cosas si todas las adquisiciones de recursos tienen éxito es uno de los casos de uso que contextlib.ExitStack()
está diseñado para manejar.
Por ejemplo (usando addCleanup()
lugar de una tearDown()
personalizada de tearDown()
):
def setUp(self):
with contextlib.ExitStack() as stack:
self._resource = stack.enter_context(GetResource())
self.addCleanup(stack.pop_all().close)
Ese es el enfoque más sólido, ya que maneja correctamente la adquisición de múltiples recursos:
def setUp(self):
with contextlib.ExitStack() as stack:
self._resource1 = stack.enter_context(GetResource())
self._resource2 = stack.enter_context(GetOtherResource())
self.addCleanup(stack.pop_all().close)
Aquí, si GetOtherResource()
falla, el primer recurso será limpiado inmediatamente por la instrucción with, mientras que si tiene éxito, la llamada pop_all()
pospondrá la limpieza hasta que se ejecute la función de limpieza registrada.
Si sabes que solo vas a tener que administrar un recurso, puedes omitir el enunciado con:
def setUp(self):
stack = contextlib.ExitStack()
self._resource = stack.enter_context(GetResource())
self.addCleanup(stack.close)
Sin embargo, eso es un poco más propenso a errores, ya que si agrega más recursos a la pila sin cambiar primero a la versión basada en declaraciones, los recursos asignados con éxito podrían no limpiarse rápidamente si fallan las adquisiciones de recursos posteriores.
También puede escribir algo comparable usando una tearDown()
personalizada de tearDown()
guardando una referencia a la pila de recursos en el caso de prueba:
def setUp(self):
with contextlib.ExitStack() as stack:
self._resource1 = stack.enter_context(GetResource())
self._resource2 = stack.enter_context(GetOtherResource())
self._resource_stack = stack.pop_all()
def tearDown(self):
self._resource_stack.close()
pytest
accesorios pytest
están muy cerca de tu idea / estilo, y permiten exactamente lo que quieres:
import pytest
from code.to.test import foo
@pytest.fixture(...)
def resource():
with your_context_manager as r:
yield r
def test_foo(resource):
assert foo(resource).bar() == 42