12

The Python documentation for unittest implies that the assertRaises() method can be used as a context manager. The code below shows gives a simple example of the unittest from the Python docs. The assertRaises() call in the testsample() method works fine.

Now I'd like to access the exception in when it is raised, but if I comment it out and instead uncomment the next block in which I attempt to used a context manager I get an AttributeError: __exit__ when I attempt to execute the code. This happens for both Python 2.7.2 and 3.2.2. I could catch the exception in a try...except block and access it that way but the documentation for unittest seems to imply the context manager would do this as well.

Is there something else I'm doing wrong here?

class TestSequenceFunctions(unittest.TestCase):

    def setUp(self):
        self.seq = [x for x in range(10)]

    def testshuffle(self):
        # make sure the shuffled sequence does not lose any elements
        random.shuffle(self.seq)
        self.seq.sort()
        self.assertEqual(self.seq, [x for x in range(10)])

    def testchoice(self):
        element = random.choice(self.seq)
        self.assert_(element in self.seq)

    def testsample(self):
        self.assertRaises(ValueError, random.sample, self.seq, 20)

        # with self.assertRaises(ValueError, random.sample, self.seq, 20):
        #     print("Inside cm")

        for element in random.sample(self.seq, 5):
            self.assert_(element in self.seq)

if __name__ == '__main__':
    unittest.main()
Blender
  • 289,723
  • 53
  • 439
  • 496
Paul Joireman
  • 2,689
  • 5
  • 25
  • 33

4 Answers4

28

It seems no-one has yet suggested:

import unittest
# For python < 2.7, do import unittest2 as unittest

class Class(object):
    def should_raise(self):
        raise ValueError('expected arg')

class test_Class(unittest.TestCase):
    def test_something(self):
        DUT = Class()
        with self.assertRaises(ValueError) as exception_context_manager:
            DUT.should_raise()
        exception = exception_context_manager.exception

        self.assertEqual(exception.args, ('expected arg', ))

I usually use e_cm as short for exception_context_manager.

NeilenMarais
  • 2,949
  • 1
  • 25
  • 23
  • 1
    Works OK for me. Remember that you have to do it inside a method of a unittest.Testcase instance. If you are using an older version of python you also need to install and use unittest2. I will expand my answer to show the full example. – NeilenMarais Jul 22 '13 at 08:56
  • You saved me tons of searching. Thanks. – rantanplan Nov 20 '13 at 18:20
  • 2
    This is actually a better option than the accepted answer, IMHO. At first I thought it wasn't working on my python 2.7 as well, until I realized you can't acces `context_manager.exception` from INSIDE the `which` block, like I was trying. The code in this answer is correct, though, and works flawlessly. Thanks for this. – malvim Aug 15 '14 at 17:34
  • @NeilenMarais - Can you please help me in a related question ? https://stackoverflow.com/questions/39909935/how-do-you-show-an-error-message-when-a-test-does-not-throw-an-expected-exceptio – Erran Morad Oct 07 '16 at 17:11
  • @BoratSagdiyev It looks like you already have an accepted answer, is there something you would like me to add? – NeilenMarais Oct 10 '16 at 10:28
  • @NeilenMarais - Thanks. If you'd like to add your own answer, then please do so. If not, then can you please check if the existing answer could be improved in any way ? – Erran Morad Oct 13 '16 at 16:22
7

The source code for unittest doesn't show an exception hook for assertRaises:

class _AssertRaisesContext(object):
    """A context manager used to implement TestCase.assertRaises* methods."""

    def __init__(self, expected, test_case, expected_regexp=None):
        self.expected = expected
        self.failureException = test_case.failureException
        self.expected_regexp = expected_regexp

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, tb):
        if exc_type is None:
            try:
                exc_name = self.expected.__name__
            except AttributeError:
                exc_name = str(self.expected)
            raise self.failureException(
                "{0} not raised".format(exc_name))
        if not issubclass(exc_type, self.expected):
            # let unexpected exceptions pass through
            return False
        self.exception = exc_value # store for later retrieval
        if self.expected_regexp is None:
            return True

        expected_regexp = self.expected_regexp
        if isinstance(expected_regexp, basestring):
            expected_regexp = re.compile(expected_regexp)
        if not expected_regexp.search(str(exc_value)):
            raise self.failureException('"%s" does not match "%s"' %
                     (expected_regexp.pattern, str(exc_value)))
        return True

So, as you suspected, forming your own try/except block is the way to go if you want to intercept the exception while still keeping the assertRaises test:

def testsample(self):
    with self.assertRaises(ValueError):
         try:
             random.sample(self.seq, 20)
         except ValueError as e:
             # do some action with e
             self.assertEqual(e.args,
                              ('sample larger than population',))
             # now let the context manager do its work
             raise                    
Raymond Hettinger
  • 216,523
  • 63
  • 388
  • 485
  • Thanks, this works but it seems like this is backwards from the way the documentation is describing it unless I'm reading it incorrectly. It doesn't really seem like you'd need the context manager in the corrected code from R Hettinger. What do you gain from the context manager? You've already caught the exception for testing. If the exception didn't occur you could always signal this in other clauses tied to the exception clause. – Paul Joireman Nov 21 '11 at 17:57
  • @PaulJoireman: Yes, you could do this without assertRaises if you preferred. The context manager just makes the simple case (check that a particular exception is raised) easier to read. – Thomas K Nov 21 '11 at 18:57
  • Can you please help me in a related question ? https://stackoverflow.com/questions/39909935/how-do-you-show-an-error-message-when-a-test-does-not-throw-an-expected-exceptio – Erran Morad Oct 07 '16 at 17:10
3

According to the documentation:

If called with callableObj omitted or None, will return a context object

So that code should be:

with self.assertRaises(ValueError):
    random.sample(self.seq, 20)
Jasmijn
  • 9,370
  • 2
  • 29
  • 43
  • 1
    If I'm understanding the OP's question correctly, it looks like he wants to intercept the exception and do additional work with it (possibly asserting the underlying message or some such) , so I don't think this answer helps. – Raymond Hettinger Nov 21 '11 at 17:21
1

Given this was asked six years ago I imagine this is something which works now but didn't work then. The docs state this appeared in 2.7 but not which micro version.

import unittest

class TestIntParser(unittest.TestCase):

    def test_failure(self):
        failure_message = 'invalid literal for int() with base 10'
        with self.assertRaises(ValueError) as cm:
            int('forty two')
        self.assertIn(failure_message, cm.exception.message)

if __name__ == '__main__':
    unittest.main()
Samuel Harmer
  • 4,264
  • 5
  • 33
  • 67
  • 1
    This is almost exactly the same as https://stackoverflow.com/a/13585289/1165509 answered in 2012. – marcanuy Mar 26 '18 at 22:55