使用Python上下文管理器与assertRaises

12
Python文档中的unittest模块暗示可以使用assertRaises()方法作为上下文管理器。下面的代码展示了Python文档中unittest模块的一个简单例子。在testsample()方法中的assertRaises()调用正常工作。 现在我想要在引发异常时访问它,但是如果我将其注释掉并取消下一个块的注释,在其中尝试使用上下文管理器,当我尝试执行代码时会出现AttributeError: __exit__。这适用于Python 2.7.2和3.2.2。我可以在try...except块中捕获异常并以那种方式访问它,但是unittest的文档似乎也暗示上下文管理器将这样做。 这里还有其他我做错了吗?
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()
4个回答

28

似乎还没有人提出:

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

我通常使用e_cm作为exception_context_manager的简称。


1
对我来说运行正常。请记住,您必须在unittest.Testcase实例的方法内执行此操作。如果您使用的是较旧版本的Python,则还需要安装并使用unittest2。我将扩展我的答案以显示完整示例。 - NeilenMarais
你帮我省去了大量的搜索时间,谢谢。 - rantanplan
2
在我看来,这实际上比被接受的答案更好。起初,我认为它在我的Python 2.7上也无法工作,直到我意识到你不能从which块内部访问context_manager.exception,就像我一直在尝试的那样。不过,这个答案中的代码是正确的,并且可以完美地工作。感谢您提供这个答案。 - malvim
@NeilenMarais - 你能帮我解决一个相关的问题吗?https://stackoverflow.com/questions/39909935/how-do-you-show-an-error-message-when-a-test-does-not-throw-an-expected-exceptio - Erran Morad
@BoratSagdiyev 看起来你已经有了一个被接受的答案,你还需要我添加什么吗? - NeilenMarais
@NeilenMarais - 谢谢。如果您想添加自己的答案,请这样做。如果不是,请检查现有答案是否可以在任何方面进行改进? - Erran Morad

7

unittest的源代码中没有为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

所以,正如你所怀疑的那样,如果你想拦截异常同时仍然保持assertRaises测试,则组成自己的try/except块是正确的方法:

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                    

谢谢,这个可行,但是似乎与文档描述的方式相反,除非我读错了。从R Hettinger的更正代码中看来,似乎并不需要上下文管理器。你从上下文管理器中获得了什么?你已经捕获了异常进行测试。如果没有发生异常,你可以在与异常子句相关联的其他子句中发出信号。 - Paul Joireman
@PaulJoireman:是的,如果您愿意,您可以在不使用assertRaises的情况下完成此操作。上下文管理器只是使简单情况(检查是否引发特定异常)更易于阅读。 - Thomas K
你能帮我解决一个相关的问题吗?https://stackoverflow.com/questions/39909935/how-do-you-show-an-error-message-when-a-test-does-not-throw-an-expected-exceptio - Erran Morad

3
根据文档:
如果省略或将callableObj设置为None,则会返回上下文对象。
所以代码应该是这样的:
with self.assertRaises(ValueError):
    random.sample(self.seq, 20)

1
如果我正确理解了OP的问题,他似乎想要拦截异常并对其进行额外的处理(可能是断言底层消息或类似的操作),因此我认为这个答案并没有帮助。 - Raymond Hettinger

1
鉴于此问题是六年前提出的,我想现在应该可以解决,但那时可能还不行。 docs 中说明了这个功能出现在2.7中,但没有说明是哪个微版本。
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()

1
这几乎与2012年https://dev59.com/l2sy5IYBdhLWcg3w8Shc#13585289的回答完全相同。 - marcanuy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接