关于单元测试:如何测试Python函数抛出异常?

How do you test that a Python function throws an exception?

如何编写只有在函数没有抛出预期异常时才会失败的单元测试?


使用unittest模块中的TestCase.assertRaises(或TestCase.failUnlessRaises),例如:

1
2
3
4
5
import mymod

class MyTestCase(unittest.TestCase):
    def test1(self):
        self.assertRaises(SomeCoolException, mymod.myfunc)


由于Python 2.7,你可以使用上下文管理器来获取实际抛出的异常对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import unittest

def broken_function():
    raise Exception('This is broken')

class MyTestCase(unittest.TestCase):
    def test(self):
        with self.assertRaises(Exception) as context:
            broken_function()

        self.assertTrue('This is broken' in context.exception)

if __name__ == '__main__':
    unittest.main()

http://docs.python.org/dev/library/unittest.html#unittest.TestCase.assertRaises

在Python 3.5中,必须将context.exception封装在str中,否则将得到TypeError

1
self.assertTrue('This is broken' in str(context.exception))


我之前的答案中的代码可以简化为:

1
2
def test_afunction_throws_exception(self):
    self.assertRaises(ExpectedException, afunction)

如果一个函数接受争论,就把它们变成这样的断言:

1
2
def test_afunction_throws_exception(self):
    self.assertRaises(ExpectedException, afunction, arg1, arg2)


How do you test that a Python function throws an exception?

How does one write a test that fails only if a function doesn't throw
an expected exception?

短答:

使用self.assertRaises方法作为上下文管理器:

1
2
3
    def test_1_cannot_add_int_and_str(self):
        with self.assertRaises(TypeError):
            1 + '1'

示范

最佳实践方法很容易在Python shell中演示。

unittest

在Python 2.7或3中:

1
import unittest

在Python 2.6中,可以安装2.7的unittest库的backport,名为unittest2,并将其别名为unittest:

1
import unittest2 as unittest

示例测试

现在,将以下测试Python类型安全性的测试粘贴到Python shell中:

1
2
3
4
5
6
7
class MyTestCase(unittest.TestCase):
    def test_1_cannot_add_int_and_str(self):
        with self.assertRaises(TypeError):
            1 + '1'
    def test_2_cannot_add_int_and_str(self):
        import operator
        self.assertRaises(TypeError, operator.add, 1, '1')

Test one使用assertRaises作为上下文管理器,确保在记录错误时正确捕获并清除错误。

我们也可以在没有上下文管理器的情况下编写它,参见测试2。第一个参数是您希望引发的错误类型,第二个参数是您正在测试的函数,其余的args和关键字args将传递给该函数。

我认为仅仅使用上下文管理器就会更加简单、可读和可维护。

运行的测试

运行测试:

1
unittest.main(exit=False)

在Python 2.6中,您可能需要以下内容:

1
unittest.TextTestRunner().run(unittest.TestLoader().loadTestsFromTestCase(MyTestCase))

你的终端机应输出以下内容:

1
2
3
4
5
6
..
----------------------------------------------------------------------
Ran 2 tests in 0.007s

OK
<unittest2.runner.TextTestResult run=2 errors=0 failures=0>

正如我们所期望的,尝试添加一个1和一个'1'会得到一个TypeError

要获得更详细的输出,请尝试以下操作:

1
unittest.TextTestRunner(verbosity=2).run(unittest.TestLoader().loadTestsFromTestCase(MyTestCase))

您的代码应该遵循以下模式(这是一个unittest模块风格的测试):

1
2
3
4
5
6
7
8
9
def test_afunction_throws_exception(self):
    try:
        afunction()
    except ExpectedException:
        pass
    except Exception as e:
       self.fail('Unexpected exception raised:', e)
    else:
       self.fail('ExpectedException not raised')

在Python < 2.7中,这个构造对于检查预期异常中的特定值很有用。unittest函数assertRaises只检查是否引发异常。


来自:http://www.lengrand.fr/2011/12/pythonunittest-assertraises-raises-error/

首先,这里是文件dum_function.py中对应的(still dum:p)函数:

1
2
3
4
5
6
7
8
9
10
def square_value(a):
  """
   Returns the square value of a.
  """

   try:
       out = a*a
   except TypeError:
       raise TypeError("Input should be a string:")

   return out

下面是要执行的测试(只插入这个测试):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import dum_function as df # import function module
import unittest
class Test(unittest.TestCase):
  """
      The class inherits from unittest
     """

   def setUp(self):
      """
       This method is called before each test
      """

       self.false_int ="A"

   def tearDown(self):
      """
       This method is called after each test
      """

       pass
      #---
         ## TESTS
   def test_square_value(self):
       # assertRaises(excClass, callableObj) prototype
       self.assertRaises(TypeError, df.square_value(self.false_int))

   if __name__ =="__main__":
       unittest.main()

我们现在准备测试我们的功能!下面是运行测试时发生的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
======================================================================
ERROR: test_square_value (__main__.Test)
----------------------------------------------------------------------
Traceback (most recent call last):
  File"test_dum_function.py", line 22, in test_square_value
    self.assertRaises(TypeError, df.square_value(self.false_int))
  File"/home/jlengrand/Desktop/function.py", line 8, in square_value
    raise TypeError("Input should be a string:")
TypeError: Input should be a string:

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (errors=1)

类型错误是actullay引发的,并生成测试失败。问题是,这正是我们想要的行为:s。

为了避免这个错误,只需在测试调用中使用lambda运行函数:

1
self.assertRaises(TypeError, lambda: df.square_value(self.false_int))

最终输出:

1
2
3
4
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

完美!

…对我来说也是完美的!!

非常感谢Julien Lengrand-Lambert先生


您可以构建自己的contextmanager来检查是否引发了异常。

1
2
3
4
5
6
7
8
9
10
import contextlib

@contextlib.contextmanager
def raises(exception):
    try:
        yield
    except exception as e:
        assert True
    else:
        assert False

然后你可以像这样使用raises:

1
2
3
4
5
with raises(Exception):
    print"Hola"  # Calls assert False

with raises(Exception):
    raise Exception  # Calls assert True

如果您使用的是pytest,那么这个东西已经实现了。你可以做pytest.raises(Exception):

例子:

1
2
3
def test_div_zero():
    with pytest.raises(ZeroDivisionError):
        1/0

结果:

1
2
3
4
5
6
pigueiras@pigueiras$ py.test
================= test session starts =================
platform linux2 -- Python 2.6.6 -- py-1.4.20 -- pytest-2.5.2 -- /usr/bin/python
collected 1 items

tests/test_div_zero.py:6: test_div_zero PASSED


我几乎在所有地方都使用doctest[1],因为我喜欢同时记录和测试函数。

请看这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def throw_up(something, gowrong=False):
   """
    >>> throw_up('Fish n Chips')
    Traceback (most recent call last):
    ...
    Exception: Fish n Chips

    >>> throw_up('Fish n Chips', gowrong=True)
    'I feel fine!'
   """

    if gowrong:
        return"I feel fine!"
    raise Exception(something)

if __name__ == '__main__':
    import doctest
    doctest.testmod()

如果您将这个例子放在一个模块中,并从命令行运行它,那么两个测试用例都将被评估和检查。

[1] Python文档:23.2 doctest——测试交互式Python示例


查看一下unittest模块的assertRaises方法。


我刚刚发现,模拟库(在它的unittest中)提供了一个assertRaisesWithMessage()方法。TestCase子类),它不仅会检查预期的异常是否被引发,还会检查它是否被预期的消息引发:

1
2
3
4
5
6
7
8
9
from testcase import TestCase

import mymod

class MyTestCase(TestCase):
    def test1(self):
        self.assertRaisesWithMessage(SomeCoolException,
                                     'expected message',
                                     mymod.myfunc)


您可以使用来自unittest模块的断言

1
2
3
4
5
6
7
8
9
10
11
import unittest

class TestClass():
  def raises_exception(self):
    raise Exception("test")

class MyTestCase(unittest.TestCase):
  def test_if_method_raises_correct_exception(self):
    test_class = TestClass()
    # note that you dont use () when passing the method to assertRaises
    self.assertRaises(Exception, test_class.raises_exception)