В python есть ли хорошая идиома для использования контекстных менеджеров в setup / teardown - PullRequest
43 голосов
/ 07 декабря 2011

Я обнаружил, что я использую множество менеджеров контекста в 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 здесь - это более общий случай, чем конкретный случай, - когда контекстные менеджеры - это способ, которым объект предназначен для перехода в заблокированное состояние и из него.

Ответы [ 5 ]

32 голосов
/ 24 июня 2012

Как насчет переопределения 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 экземпляр там.

15 голосов
/ 22 августа 2017

Управление менеджерами контекста в ситуациях, когда вы не хотите, чтобы оператор 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()

В качестве альтернативы, вы также можетеопределить пользовательскую функцию очистки, которая обращается к ресурсу через ссылку закрытия, избегая необходимости сохранять любое дополнительное состояние в тестовом примере исключительно для целей очистки:

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)
5 голосов
/ 16 декабря 2011

Проблема с вызовами __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», а также методы и функции, чтобы я сам мог протестировать декоратор и метакласс, который я предлагаю для этого ответа)

3 голосов
/ 23 августа 2017

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
2 голосов
/ 09 февраля 2012

Я бы сказал, что вы должны отделить свой тест менеджера контекста от теста класса 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()

Это упрощает код, предотвращает смешивание задач и позволяет повторно использовать менеджер контекста без необходимости кодировать его во многих местах.

...