tutorial tutor programación programacion gui español ejemplos crear con acodigo python unit-testing contextmanager

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