69

I am finding that I am using plenty of context managers in Python. However, I have been testing a number of things using them, and I am often needing the following:

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')

When this gets to many tests, this is clearly going to get boring, so in the spirit of SPOT/DRY (single point of truth/dont repeat yourself), I'd want to refactor those bits into the test setUp() and tearDown() methods.

However, trying to do that has lead to this ugliness:

  def setUp(self):
    self._resource = GetSlot()
    self._resource.__enter__()

  def tearDown(self):
    self._resource.__exit__(None, None, None)

There must be a better way to do this. Ideally, in the setUp()/tearDown() without repetitive bits for each test method (I can see how repeating a decorator on each method could do it).

Edit: Consider the undertest object to be internal, and the GetResource object to be a third party thing (which we aren't changing).

I've renamed GetSlot to GetResource here—this is more general than specific case—where context managers are the way which the object is intended to go into a locked state and out.

Zearin
  • 1,474
  • 2
  • 17
  • 36
Danny Staple
  • 7,101
  • 4
  • 43
  • 56
  • 1
    I don't understand the issue with your `setUp`/`tearDown` methods, looks perfectly fine to me. I suppose an alternative would be to create a decorator that uses the `with` statement and apply it automatically to all methods, but that would be more work for no real benefit. – interjay Dec 07 '11 at 13:56
  • 1
    I suppose it is that I view the '__' methods as private and "magic" methods that shouldn't be explicitly called. However, given that this is in a test context, perhaps this will suffice. – Danny Staple Dec 07 '11 at 14:20
  • 1
    The setup and teardown is the cleaner of the two. I would think that GetSlot should have the proper API to be used without the context manager. The fact that you're struggling with finding the cleanest way to do this proves that GetSlot needs work. Unless GetSlot is not your code, in which case I take it all back. – Chris Lacasse Dec 07 '11 at 15:38
  • 3
    Go for your existing solution, it is totally valid to call "magic" methods within test cases. – Ferdinand Beyer Dec 07 '11 at 17:12
  • If the code is not my code, I could wrap it such that I have a clean outside API, although perhaps I am prepared to take @FerdinandBeyer's as a suitable answer. Add these as answers (and I can credit them). – Danny Staple Dec 08 '11 at 11:49
  • https://pytypest.orsinium.dev/fixture.html – Andrew Jun 06 '23 at 17:06

6 Answers6

49

How about overriding unittest.TestCase.run() as illustrated below? This approach doesn't require calling any private methods or doing something to every method, which is what the questioner wanted.

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()

This approach also allows passing the TestCase instance to the context manager, if you want to modify the TestCase instance there.

Willi Mentzel
  • 27,862
  • 20
  • 113
  • 121
cjerdonek
  • 5,814
  • 2
  • 32
  • 26
  • I like this - it is a simple solution. – Danny Staple Apr 22 '13 at 13:28
  • I think the answer it's unclear. Explain better that the test() method is executed inside the context, and that doStuff() or doOtherStuff() can be executed before asserting something. – Alan Franzoni Apr 23 '13 at 22:43
  • will, with this way, `unittest` evaluate the occurrence of an exception inside the context manager's `__enter__` or `__exit__`, in the same way as it would evaluate the occurrence of an exception in the `setUp` or `tearDown` methods, respectively? – n611x007 Jan 19 '15 at 15:18
  • I came across this while trying to understand http://flask.pocoo.org/snippets/26/ where there it seems `__call__` is used similar to `run`. I wonder what the difference is between overriding `__call__` vs `run`? – BenjaminGolder Oct 31 '15 at 00:38
  • 1
    @BenjaminGolder `TestCase.__call__` is defined as `return self.run(*args, **kwds)` so the difference would seem to be stylistic. – Brian M. Hunt Mar 17 '16 at 14:28
  • Good answer. Upvoted. FWIW, alas, this won't work in async scenario. – RayLuo Mar 07 '20 at 20:01
  • 1
    The order is all wrong. If you derive MyTest from a lot of mixins that each have setUp()/tearDown() and now also run() methods then it first executes all the run(), then all the setUp(), then all the tearDown(). But we need the context to span between setUp() and tearDown() of the specific mixin it belongs to. – Velkan Jan 14 '21 at 09:06
37

Manipulating context managers in situations where you don't want a with statement to clean things up if all your resource acquisitions succeed is one of the use cases that contextlib.ExitStack() is designed to handle.

For example (using addCleanup() rather than a custom tearDown() implementation):

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

That's the most robust approach, since it correctly handles acquisition of multiple resources:

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)

Here, if GetOtherResource() fails, the first resource will be cleaned up immediately by the with statement, while if it succeeds, the pop_all() call will postpone the cleanup until the registered cleanup function runs.

If you know you're only ever going to have one resource to manage, you can skip the with statement:

def setUp(self):
    stack = contextlib.ExitStack()
    self._resource = stack.enter_context(GetResource())
    self.addCleanup(stack.close)

However, that's a bit more error prone, since if you add more resources to the stack without first switching to the with statement based version, successfully allocated resources may not get cleaned up promptly if later resource acquisitions fail.

You can also write something comparable using a custom tearDown() implementation by saving a reference to the resource stack on the test case:

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()

Alternatively, you can also define a custom cleanup function that accesses the resource via a closure reference, avoiding the need to store any extra state on the test case purely for cleanup purposes:

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)
ncoghlan
  • 40,168
  • 10
  • 71
  • 80
  • 2
    +1 This is definitely nicer than overriding `run`. The only downside is that the context manager can't process exceptions when it's closed like this. – Neil G Aug 22 '17 at 17:01
  • 2
    The last `tearDown` example is a bit dangerous, because if `setUp` fails (after the example `with` statement), `tearDown` is not called. Prefer [`addCleanup`](https://docs.python.org/3/library/unittest.html#unittest.TestCase.addCleanup). – user1338062 Sep 13 '18 at 05:59
  • An example [with a local function](http://pythontesting.net/framework/unittest/unittest-fixtures/#cleanup) would be helpful here, e.g. `def cleaner():` `stack.pop_all().close()` `self.addCleanup(cleaner)` – Bob Stein May 20 '19 at 00:22
  • @BobStein I'm not sure I follow, as that code is equivalent to the first example, just with an extra wrapper function as the callback rather than passing in the bound `close` method directly. You certainly *can* do that if you prefer, but it's slightly slower for no practical benefit. – ncoghlan May 31 '19 at 10:08
  • 1
    Not in that code, you're right. But many cleanups will need more than one function call. (My case that brought me here needed 2 calls, 1 conditional.) So a wrapper would demonstrate this technique with more expandability. Also, functions as objects is already a freakish idea for the mind. Better [explicit](https://www.python.org/dev/peps/pep-0020/) then, perhaps. I'm going to now edit your answer with an example wrapper just to clarify. Please feel free to revert or edit as you see fit. I got a lot out of your `addCleanup()` point but maybe that's tangential to your `ExitStack()` point. – Bob Stein May 31 '19 at 15:02
  • 1
    @BobStein Ah, now I get it. Yeah, storing state on a closure can be a nice alternative to storing it on the test case. – ncoghlan Jun 03 '19 at 00:07
12

pytest fixtures are very close to your idea/style, and allow for exactly what you want:

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
Dima Tisnek
  • 11,241
  • 4
  • 68
  • 120
5

The problem with calling __enter__ and __exit__ as you did, is not that you have done so: they can be called outside of a with statement. The problem is that your code has no provision to call the object's __exit__ method properly if an exception occurs.

So, the way to do it is to have a decorator that will wrap the call to your original method in a withstatement. A short metaclass can apply the decorator transparently to all methods named test* in the class -

# -*- 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()

(I also included mock implementations of "GetSlot" and the methods and functions in your example so that I myself could test the decorator and metaclass I am suggesting on this answer)

jsbueno
  • 99,910
  • 10
  • 151
  • 209
  • 1
    Referring to python docs: http://docs.python.org/library/unittest.html#unittest.TestCase.tearDown, tearDown is called after every test, even when failure cases and exceptions have been raised (and setUp before every test). The MetaClass/MetaContext is surely only going to enter/exit when MyTestCase as a whole test case is run, not the individual units. This could mean there are interactions between tests. – Danny Staple Dec 17 '11 at 09:45
  • @DannyStaple: as for my code: I wrap each individual test method in a decorator which calls teh text within a `with`statement - both enter and exit are run at each test execution. Insert some print statements on the test_wrapper function and see for yourself. As fro your original code, it is nice to know `.__exit__` will be called. Technicalle `.__exit__` should be passed information about the exceptiontaht ocurred, but I agree this should not be an issue in most cases. – jsbueno Dec 17 '11 at 11:52
  • Ah I see - your hitting the objects dict and applying the context to each item which starts with "test", which would be the test methods. The @wraps decorator is handy to know -I've had the issue of loosing the methods name when wrapping before. – Danny Staple Dec 18 '11 at 16:37
2

I'd argue you should separate your test of the context manager from your test of the Slot class. You could even use a mock object simulating the initialize/finalize interface of slot to test the context manager object, and then test your slot object separately.

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()

This makes code simpler, prevents concern mixing and allows you to reuse the context manager without having to code it in many places.

Alan Franzoni
  • 3,041
  • 1
  • 23
  • 35
  • I think mocking is probably the solution here, so I'll give you a +1 for this. The original code (in a number of cases - I've now generalized the question a little) allowed you to construct something, but it wasn't locked until going into context. Note that in the original question - neither the Context, nor the Slot are the SUT (Subject under test)- they are resources. – Danny Staple Feb 13 '12 at 18:11
  • Coming back to this now, I think mocking the resource is exactly what I would now do, as looking at the above code, as you have pointed out, the resource is not under test at all. You may want to not have any side effect the resource could have by being used. I'd mock only the resource interface that the SUT required and may even create helpers to construct the mock resource if I needed it a lot. – Danny Staple Jun 24 '12 at 20:54
  • This answer still leaves open the question of how to apply a context manager in a DRY way to the tests in a TestCase class. – cjerdonek Jun 25 '12 at 00:24
  • cjerdonek: setUp/tearDown itself *is* a context manager behaviour, even though it's different in style from Python with syntax. If you *must* use a context manager then it's probably a matter of API, and you should decouple the context manager from the actual code. And, BTW, the setUp/tearDown solution proposed by Danny, although ugly, should work 100%. – Alan Franzoni Jun 26 '12 at 07:50
  • 1
    Alan, I understand that Danny's code works, but his "solution" was part of his question. He was asking for a less ugly way to apply a context manager in a DRY way to the tests in a TestCase class (e.g. one that doesn't require accessing the private methods of a context manager). Perhaps my comment should have read more simply that this answer still leaves unanswered his original question. I offered a way to do this which allows one to use the context manager as-is. – cjerdonek Jun 28 '12 at 08:18
1

Looks like this discussion is still relevant 10 years later! To add to @ncoghlan's excellent answer it looks like unittest.TestCase added this exact functionality via the enterContext helper method as of python 3.11! From the docs:

enterContext(cm)

Enter the supplied context manager. If successful, also add its __exit__() method as a cleanup function by addCleanup() and return the result of the __enter__() method.

New in version 3.11.

It looks like this precludes the need to manually addCleanup() to close the stack of context managers, as it's added when you provide the context manager to enterContext. So it seems like all that's needed nowadays is:

def setUp(self):
    self._resource = GetResource() # if you need a reference to it in tests
    self.enterContext(GetResource())
    # self._resource implicitly released during cleanups after tearDown()

(I guess unittest got tired of everyone flocking to pytest because of their helpful fixtures)

starball
  • 20,030
  • 7
  • 43
  • 238
BentHam
  • 36
  • 5