web-dev-qa-db-ja.com

Pythonでは、セットアップ/ティアダウンでコンテキストマネージャーを使用するための良いイディオムがありますか

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()で(各メソッドでデコレータを繰り返すことでどのように実行できるかがわかります)。

編集:アンダーテストオブジェクトは内部オブジェクトであり、GetResourceオブジェクトはサードパーティのものであると見なします(これは変更していません)。

ここでは、GetSlotの名前をGetResourceに変更しました。これは、特定の場合よりも一般的です。コンテキストマネージャーは、オブジェクトがロック状態になり、ロック状態から抜け出すための方法です。

49
Danny Staple

以下に示すように、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インスタンスをコンテキストマネージャーに渡すこともできます。

32
cjerdonek

すべてのリソース取得が成功した場合にwithステートメントでクリーンアップしたくない状況でコンテキストマネージャーを操作することは、 contextlib.ExitStack() のユースケースの1つです。処理するように設計されています。

例(カスタムのaddCleanup()実装ではなくtearDown()を使用):

_def setUp(self):
    with contextlib.ExitStack() as stack:
        self._resource = stack.enter_context(GetResource())
        self.addCleanup(stack.pop_all().close)
_

これは、複数のリソースの取得を正しく処理するため、最も堅牢なアプローチです。

_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()呼び出しは、登録されたクリーンアップ関数が実行されるまでクリーンアップを延期します。

管理するリソースが1つしかないことがわかっている場合は、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)
_
16
ncoghlan

__enter____exit__を呼び出したときの問題は、そうしているということではありません。これらはwithステートメントの外部で呼び出すことができます。問題は、例外が発生した場合にオブジェクトの__exit__メソッドを適切に呼び出すためのプロビジョニングがコードにないことです。

したがって、これを行う方法は、元のメソッドへの呼び出しをwithstatementでラップするデコレータを用意することです。短いメタクラスは、クラス内の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」のモック実装とメソッドと関数も例に含めました)

5
jsbueno

pytestフィクスチャはあなたのアイデア/スタイルに非常に近く、あなたが望むものを正確に可能にします:

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

コンテキストマネージャーのテストを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 __== "__main__":
    main()

これにより、コードが単純になり、懸念の混合が防止され、多くの場所でコーディングしなくてもコンテキストマネージャーを再利用できます。

2
Alan Franzoni