在python中,是否有一个在安装/拆卸中使用上下文管理器的好习惯用法

在python中,是否有一个在安装/拆卸中使用上下文管理器的好习惯用法,python,unit-testing,contextmanager,Python,Unit Testing,Contextmanager,我发现我在Python中使用了大量的上下文管理器。然而,我一直在使用它们测试很多东西,我经常需要以下几点: class MyTestCase(unittest.TestCase): def testFirstThing(self): with GetResource() as resource: u = UnderTest(resource) u.doStuff() self.assertEqual(u.getSomething(), 'a val

我发现我在Python中使用了大量的上下文管理器。然而,我一直在使用它们测试很多东西,我经常需要以下几点:

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')
当这涉及到许多测试时,这显然会变得无聊,因此本着SPOT/DRY(单点真理/不要重复你自己)的精神,我想将这些部分重构到测试
setUp()
tearDown()
方法中

然而,试图这样做却导致了这种丑陋:

  def setUp(self):
    self._resource = GetSlot()
    self._resource.__enter__()

  def tearDown(self):
    self._resource.__exit__(None, None, None)
必须有更好的方法来做到这一点。理想情况下,在
setUp()
/
tearDown()
中,每个测试方法都没有重复的位(我可以看到在每个方法上重复一个decorator是如何做到的)

< > >强>编辑:< /强>认为未测试对象是内部的,而<代码> GETReals< /Cord>对象是第三方的东西(我们没有改变)。
我已将
GetSlot
重命名为
GetResource
,这比上下文管理器是对象进入锁定状态和退出的方式的特定情况更为一般。

调用
\uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu
的问题,并不是说您已经这样做了:它们可以在
with
语句之外调用。问题是,如果发生异常,代码没有规定正确调用对象的
\uuuuuuuuuuuuuuuuuuuuuuuuuuuu
方法

因此,实现这一点的方法是使用一个decorator,将对原始方法的调用封装在
with
语句中。一个较短的元类可以将decorator透明地应用于类中名为test*的所有方法-

# -*- 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()

(我还包括了“GetSlot”的模拟实现以及示例中的方法和函数,以便我自己能够测试我在这个答案中建议的装饰器和元类)

我认为您应该将上下文管理器的测试与Slot类的测试分开。您甚至可以使用模拟slot的initialize/finalize接口的模拟对象来测试上下文管理器对象,然后单独测试slot对象

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()

这使代码更简单,防止问题混合,并允许您重用上下文管理器,而无需在许多地方对其进行编码。

覆盖
unittest.TestCase.run()
如何,如下所示?这种方法不需要调用任何私有方法,也不需要对每个方法都执行某些操作,这正是提问者想要的

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()

这种方法还允许将
TestCase
实例传递给上下文管理器,如果您想修改那里的
TestCase
实例。

在您不希望使用
with
语句来清理所有资源获取成功的情况下操作上下文管理器是设计用来处理的用例之一

例如(使用
addCleanup()
而不是自定义
tearDown()
实现):

这是最稳健的方法,因为它正确地处理了多种资源的获取:

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)
在这里,如果
GetOtherResource()
失败,则with语句将立即清理第一个资源,如果成功,则
pop_all()
调用将推迟清理,直到注册的清理函数运行

如果您知道只需要管理一个资源,可以跳过with语句:

def setUp(self):
    stack = contextlib.ExitStack()
    self._resource = stack.enter_context(GetResource())
    self.addCleanup(stack.close)
但是,这更容易出错,因为如果您在没有首先切换到基于with语句的版本的情况下向堆栈添加更多资源,那么如果以后的资源获取失败,成功分配的资源可能不会得到及时清理

您还可以通过在测试用例上保存对资源堆栈的引用,使用自定义的
tearDown()
实现编写类似的内容:

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()
或者,您还可以定义一个自定义清理函数,该函数通过闭包引用访问资源,从而避免纯粹出于清理目的而在测试用例上存储任何额外状态:

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)

pytest
fixture与您的想法/风格非常接近,可以满足您的需求:

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

我不明白你的
设置
/
拆卸
方法的问题,在我看来很好。我想另一种方法是创建一个decorator,它使用
with
语句,并将其自动应用于所有方法,但这将是更多的工作,没有真正的好处。我想这是因为我将“\uuuu”方法视为私有和“神奇”方法,不应该显式调用。然而,考虑到这是在测试上下文中,这也许就足够了。安装和拆卸是两者中比较干净的。我认为GetSlot应该有适当的API,可以在没有上下文管理器的情况下使用。事实上,您正在努力寻找最干净的方法来实现这一点,这证明GetSlot需要工作。除非GetSlot不是你的代码,在这种情况下,我会把它全部收回。使用你现有的解决方案,在测试用例中调用“magic”方法是完全有效的。如果代码不是我的代码,我可以包装它,这样我就有了一个干净的外部API,尽管也许我准备把@FerdinandBeyer作为一个合适的答案。添加这些作为答案(我可以相信它们)。参考python文档:,每次测试后都会调用tearDown,即使出现了失败案例和异常(以及每次测试前的设置)。当MyTestCase作为一个完整的测试用例运行时,元类/元上下文肯定只会进入/退出,而不是单个单元。这可能意味着测试之间存在交互。@DannyStaple:至于我的代码:我将每个单独的测试方法包装在一个decorator中,该decorator调用
with
语句中的文本-每次测试执行时都会运行enter和exit。在test_包装函数上插入一些print语句,然后自己查看。对于您的原始代码,很高兴知道
\uu退出__
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