В 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()
без повторяющихся битов для каждого тестового метода (я вижу, как это может сделать повторение декоратора для каждого метода).
Изменить:. Рассмотрим объект underest как внутренний, а объект GetResource
- вещь третьей стороны (которую мы не меняем).
Я переименовал GetSlot
в GetResource
здесь - это более общий, чем специфический контекст-контекст, где контекстные менеджеры - это способ, которым объект предназначен для перехода в заблокированное состояние и выход.
Ответы
Ответ 1
Как насчет переопределения 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
там.
Ответ 2
Манипуляция менеджерами контекста в ситуациях, когда вы не хотите, чтобы оператор with
очищал вещи, если все ваши приобретения ресурсов преуспели, является одним из вариантов использования contextlib.ExitStack()
предназначен для обработки.
Например (используя 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()
отложит очистку до тех пор, пока не будет запущена зарегистрированная функция очистки.
Если вы знаете, что у вас будет только один ресурс для управления, вы можете пропустить оператор with:
def setUp(self):
stack = contextlib.ExitStack()
self._resource = stack.enter_context(GetResource())
self.addCleanup(stack.close)
Тем не менее, это немного более подвержено ошибкам, поскольку, если вы добавите больше ресурсов в стек без предварительного переключения на версию с выражением на основе инструкции, успешно выделенные ресурсы могут не быть очищены оперативно, если последующие сбои ресурсов не удастся.
Вы также можете написать что-то сопоставимое, используя специальную реализацию 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()
Ответ 3
Проблема с вызовами __enter__
и __exit__
, как вы это делали, заключается не в том, что вы это сделали: их можно вызывать вне оператора with
. Проблема в том, что ваш код не имеет права правильно обращаться к объекту __exit__
, если возникает исключение.
Итак, способ сделать это - иметь декоратор, который будет обертывать вызов исходному методу в инструкции with
. Краткий метакласс может применять декоратор прозрачно ко всем методам 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" и методы и функции в вашем примере, чтобы я сам мог протестировать декоратор и метакласс, который я предлагаю на этом ответе)
Ответ 4
Я бы сказал, что вы должны отделить свой тест от менеджера контекста от теста класса 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()
Это упрощает код, предотвращает смешивание проблем и позволяет повторно использовать диспетчер контекста без необходимости его кодирования во многих местах.
Ответ 5
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