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
に変更しました。これは、特定の場合よりも一般的です。コンテキストマネージャーは、オブジェクトがロック状態になり、ロック状態から抜け出すための方法です。
以下に示すように、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
ステートメントでクリーンアップしたくない状況でコンテキストマネージャーを操作することは、 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)
_
__enter__
と__exit__
を呼び出したときの問題は、そうしているということではありません。これらはwith
ステートメントの外部で呼び出すことができます。問題は、例外が発生した場合にオブジェクトの__exit__
メソッドを適切に呼び出すためのプロビジョニングがコードにないことです。
したがって、これを行う方法は、元のメソッドへの呼び出しをwith
statementでラップするデコレータを用意することです。短いメタクラスは、クラス内の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」のモック実装とメソッドと関数も例に含めました)
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
コンテキストマネージャーのテストを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()
これにより、コードが単純になり、懸念の混合が防止され、多くの場所でコーディングしなくてもコンテキストマネージャーを再利用できます。