Je constate que j'utilise beaucoup de gestionnaires de contexte en Python. Cependant, j'ai testé un certain nombre de choses en les utilisant, et j'ai souvent besoin des éléments suivants:
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')
Lorsque cela arrive à de nombreux tests, cela va clairement devenir ennuyeux, donc dans l'esprit de SPOT / DRY (point de vérité unique / ne vous répétez pas), je voudrais refactoriser ces bits dans le test setUp()
et tearDown()
méthodes.
Cependant, essayer de le faire a conduit à cette laideur:
def setUp(self):
self._resource = GetSlot()
self._resource.__enter__()
def tearDown(self):
self._resource.__exit__(None, None, None)
Il doit y avoir une meilleure façon de procéder. Idéalement, dans le setUp()
/ tearDown()
sans bits répétitifs pour chaque méthode de test (je peux voir comment répéter un décorateur sur chaque méthode pourrait le faire).
Modifier: considérez que l'objet le plus bas est interne et que l'objet GetResource
est une chose tierce (que nous ne changeons pas).
J'ai renommé GetSlot
en GetResource
ici - c'est plus général que le cas spécifique - où les gestionnaires de contexte sont la façon dont l'objet est destiné à entrer et sortir verrouillé.
5 réponses
Que diriez-vous de remplacer unittest.TestCase.run()
comme illustré ci-dessous? Cette approche ne nécessite pas d'appeler des méthodes privées ou de faire quelque chose pour chaque méthode, ce que le questionneur voulait.
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()
Cette approche permet également de passer l'instance TestCase
au gestionnaire de contexte, si vous souhaitez y modifier l'instance TestCase
.
Je dirais que vous devriez séparer votre test du gestionnaire de contexte de votre test de la classe Slot. Vous pouvez même utiliser un objet simulé simulant l'interface d'initialisation / finalisation de slot pour tester l'objet gestionnaire de contexte, puis tester votre objet slot séparément.
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()
Cela rend le code plus simple, empêche le mélange des préoccupations et vous permet de réutiliser le gestionnaire de contexte sans avoir à le coder à de nombreux endroits.
Les pytest
luminaires sont très proches de votre idée / style et permettent exactement ce que vous voulez:
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
Le problème avec l'appel de __enter__
et __exit__
comme vous l'avez fait, n'est pas que vous l'avez fait: ils peuvent être appelés en dehors d'une instruction with
. Le problème est que votre code n'a aucune disposition pour appeler correctement la méthode __exit__
de l'objet si une exception se produit.
Donc, la façon de le faire est d'avoir un décorateur qui encapsulera l'appel à votre méthode d'origine dans une instruction with
. Une courte métaclasse peut appliquer le décorateur de manière transparente à toutes les méthodes nommées test * dans la classe -
# -*- 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()
(J'ai également inclus des implémentations simulées de "GetSlot" et les méthodes et fonctions dans votre exemple afin que je puisse moi-même tester le décorateur et la métaclasse que je suggère sur cette réponse)
La manipulation des gestionnaires de contexte dans des situations où vous ne voulez pas qu'une instruction with
nettoie les choses si toutes vos acquisitions de ressources réussissent est l'un des cas d'utilisation qui contextlib.ExitStack()
est conçu pour être géré.
Par exemple (en utilisant addCleanup()
plutôt qu'une implémentation personnalisée tearDown()
):
def setUp(self):
with contextlib.ExitStack() as stack:
self._resource = stack.enter_context(GetResource())
self.addCleanup(stack.pop_all().close)
C'est l'approche la plus robuste, car elle gère correctement l'acquisition de plusieurs ressources:
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)
Ici, si GetOtherResource()
échoue, la première ressource sera immédiatement nettoyée par l'instruction with, tandis que si elle réussit, l'appel pop_all()
reportera le nettoyage jusqu'à ce que la fonction de nettoyage enregistrée s'exécute.
Si vous savez que vous n'aurez jamais qu'une seule ressource à gérer, vous pouvez ignorer l'instruction with:
def setUp(self):
stack = contextlib.ExitStack()
self._resource = stack.enter_context(GetResource())
self.addCleanup(stack.close)
Cependant, c'est un peu plus sujet aux erreurs, car si vous ajoutez plus de ressources à la pile sans passer d'abord à la version basée sur une instruction, les ressources allouées avec succès peuvent ne pas être nettoyées rapidement si les acquisitions de ressources ultérieures échouent.
Vous pouvez également écrire quelque chose de comparable à l'aide d'une implémentation personnalisée tearDown()
en enregistrant une référence à la pile de ressources sur le scénario de test:
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()
Alternativement, vous pouvez également définir une fonction de nettoyage personnalisée qui accède à la ressource via une référence de fermeture, évitant d'avoir à stocker tout état supplémentaire sur le scénario de test uniquement à des fins de nettoyage:
def setUp(self):
with contextlib.ExitStack() as stack:
resource = stack.enter_context(GetResource())
def cleanup():
if necessary:
one_last_chance_to_use(resource)
stack.pop_all().close()
self.addCleanup(cleanup)
Questions connexes
De nouvelles questions
python
Python est un langage de programmation multi-paradigme, typé dynamiquement et polyvalent. Il est conçu pour être rapide à apprendre, comprendre, utiliser et appliquer une syntaxe propre et uniforme. Veuillez noter que Python 2 est officiellement hors support à partir du 01-01-2020. Néanmoins, pour les questions Python spécifiques à la version, ajoutez la balise [python-2.7] ou [python-3.x]. Lorsque vous utilisez une variante Python (par exemple, Jython, PyPy) ou une bibliothèque (par exemple, Pandas et NumPy), veuillez l'inclure dans les balises.