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é.

47
Danny Staple 7 déc. 2011 à 17:39

5 réponses

Meilleure réponse

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.

33
Willi Mentzel 17 mai 2016 à 13:07

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.

2
Alan Franzoni 9 févr. 2012 à 14:57

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
3
Dima Tisnek 23 août 2017 à 01:57

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)

5
jsbueno 16 déc. 2011 à 02:33

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)
18
ncoghlan 3 juin 2019 à 00:06
8416208