Skip to content

Instantly share code, notes, and snippets.

@pelme
Last active December 11, 2015 22:49
Show Gist options
  • Save pelme/4671923 to your computer and use it in GitHub Desktop.
Save pelme/4671923 to your computer and use it in GitHub Desktop.
import pytest
from contextlib import contextmanager
from functools import wraps
# TODO: Make it possible to pass standard pytest.fixture args here too, like scope, params etc..
def pytest_contextfixture(fn):
ctxmgr = contextmanager(fn)
@pytest.fixture
@wraps(fn)
def actual_fixture(request, scope='function'):
ctxinst = ctxmgr()
# TODO: Proper exception propagation?
request.addfinalizer(lambda: ctxinst.__exit__(None, None, None))
return ctxinst.__enter__()
return actual_fixture
@pytest_contextfixture
def my_fixture():
# Test setup with possibly nested with-blocks can be performed here
yield 1234
# Teardown is possible here.
def test_something(my_fixture):
assert my_fixture == 1234
@pelme
Copy link
Author

pelme commented Feb 2, 2013

@RonnyPfannschmidt Yeah, it would be nicer, but then the order of @contextmanager and @pytest.fixture would be important. I think it could support both variants... i.e. support decorating "real" context managers too, like your example and also something like

@pytest.fixture
class Foo(object):
    def __enter__(self): pass
    def __exit__(self, *a, **kw): pass 

But it could also support decorating a generator, in that case it would turn it would be applied to contextmanager().

@pelme
Copy link
Author

pelme commented Feb 2, 2013

@hpk42 No, the teardown/exit executes in the right order. I have packaged this gist up at https://github.com/pelme/pytest-contextfixture with tests that prove this too!

Yes, using context managers like this breaks fixture ordering, but in my use case I am not really concerned about that.

To give a more concrete example: The context managers I am using are mostly mock patchers, where I only use function scope.

i.e.

@pytest.fixture
def some_object_to_test(request):
    an_object_to_test = Something()

    with mock.patch('some_module', 'foo'):
        with mock.patch(Something, 'bar', 'a faked value'):
            yield an_object_to_test

def test_a(some_object_to_test):
    pass

def test_b(some_object_to_test):
    pass

def test_c(some_object_to_test):
    pass

Today, I am doing something like this (which indeed works, but it feels a bit awkward):

@contextlib.contextmanager
def setup_some_object_to_test():
    an_object_to_test = Something()

    with mock.patch('some_module', 'foo'):
        with mock.patch(Something, 'bar', 'a faked value'):
            yield an_object_to_test

def test_a(some_object_to_test):
    with setup_some_object_to_test() as some_object_to_test:
        pass

def test_b():
    with setup_some_object_to_test() as some_object_to_test:
        pass

def test_c():
    with setup_some_object_to_test() as some_object_to_test:
        pass

Those mocks is not really suitable for re-use as standalone fixtures, and most of these fixtures are defined in the local test modules themselves.

For more general fixtures, they should be defined as modular fixtures themselves.

There might be a better solution all together, but I hope this shows my reasoning. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment