【问题标题】:How do you test that a Python function throws an exception?如何测试 Python 函数是否引发异常?
【发布时间】:2010-09-12 20:54:54
【问题描述】:

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

【问题讨论】:

    标签: python unit-testing exception


    【解决方案1】:

    如果您使用的是 Python 3,为了断言异常及其消息,您可以在上下文管理器中使用 assertRaises 并将消息作为 msg 关键字参数传递,如下所示:

    import unittest
    
    def your_function():
        raise RuntimeError('your exception message')
    
    class YourTestCase(unittest.TestCase):
        def test(self):
            with self.assertRaises(RuntimeError, msg='your exception message'):
                your_function()
    
    
    if __name__ == '__main__':
        unittest.main()
    

    【讨论】:

      【解决方案2】:

      如果在此类中将 stock_id 设置为 Integer 将引发错误,则会引发 TypeError,如果发生这种情况则测试将通过,否则将失败

      def set_string(prop, value):
         if not isinstance(value, str):
            raise TypeError("i told you i take strings only ")
         return value
      
      class BuyVolume(ndb.Model):
          stock_id = ndb.StringProperty(validator=set_string)
      
      from pytest import raises
      buy_volume_instance: BuyVolume = BuyVolume()
      with raises(TypeError):
        buy_volume_instance.stock_id = 25
      

      【讨论】:

        【解决方案3】:

        对于 await/async aiounittest,模式略有不同:

        https://aiounittest.readthedocs.io/en/latest/asynctestcase.html#aiounittest.AsyncTestCase

        async def test_await_async_fail(self):
            with self.assertRaises(Exception) as e:
                await async_one()
        

        【讨论】:

          【解决方案4】:

          您可以使用上下文管理器运行错误函数,并使用assertRaisesMessage 断言它引发异常并带有特定消息

          with self.assertRaisesMessage(SomeException,'Some error message e.g 404 Not Found'):
              faulty_funtion()
          
          

          【讨论】:

          • assertRaisesMessage 是仅 Django 的方法,而不是文档here 中所示的本机 Python Testcase 类方法,请编辑您的答案以澄清这一点。
          【解决方案5】:

          由于我还没有看到任何关于如何使用上下文管理器检查我们是否在接受的列表中遇到特定异常的详细说明,或者其他异常详细信息,我将添加我的(在 python 3.8 上检查)。

          如果我只是想检查函数是否正在引发例如TypeError,我会写:

          with self.assertRaises(TypeError):
              function_raising_some_exception(parameters)
          

          如果我想检查函数是否引发了TypeErrorIndexError,我会写:

          with self.assertRaises((TypeError,IndexError)):
              function_raising_some_exception(parameters)
          

          如果我想要更多关于引发的异常的详细信息,我可以在这样的上下文中捕获它:

          # Here I catch any exception    
          with self.assertRaises(Exception) as e:
              function_raising_some_exception(parameters)
          
          # Here I check actual exception type (but I could
          # check anything else about that specific exception,
          # like it's actual message or values stored in the exception)
          self.assertTrue(type(e.exception) in [TypeError,MatrixIsSingular])
          

          【讨论】:

          • 这个答案非常有用(哈!)因为类型检查和检查其他任何内容的建议。这帮助我检查了self.assertEqual(e.exception.args[0], "Values cannot be a generator. Use list(generator) instead.",) 的明确错误消息。
          • 上下文管理的错误是最好的。它们也允许您测试错误消息!
          【解决方案6】:

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

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

          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
          

          这里是要执行的测试(只插入了这个测试):

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

          我们现在可以测试我们的功能了!以下是尝试运行测试时发生的情况:

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

          TypeError 被实际引发,并生成测试失败。问题是这正是我们想要的行为:s。

          为避免此错误,只需在测试调用中使用 lambda 运行函数即可:

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

          最终输出:

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

          完美!

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

          非常感谢 Julien Lengrand-Lambert 先生


          这个测试断言实际上返回了一个误报。发生这种情况是因为 'assertRaises' 中的 lambda 是引发类型错误的单元,而不是测试的函数。

          【讨论】:

          • 请注意,您不需要 lambda。 self.assertRaises(TypeError, df.square_value(self.false_int)) 行调用方法并返回结果。您想要的是传递方法和任何参数并让单元测试调用它:self.assertRaises(TypeError, df.square_value, self.false_int)
          • 而在2021年,我的一个朋友发现我的博客被用来回答这个问题。感谢@macm 的赞誉!
          【解决方案7】:

          这里有很多答案。代码展示了我们如何创建一个异常,我们如何在我们的方法中使用该异常,最后,您如何在单元测试中验证是否引发了正确的异常。

          import unittest
          
          class DeviceException(Exception):
              def __init__(self, msg, code):
                  self.msg = msg
                  self.code = code
              def __str__(self):
                  return repr("Error {}: {}".format(self.code, self.msg))
          
          class MyDevice(object):
              def __init__(self):
                  self.name = 'DefaultName'
          
              def setParameter(self, param, value):
                  if isinstance(value, str):
                      setattr(self, param , value)
                  else:
                      raise DeviceException('Incorrect type of argument passed. Name expects a string', 100001)
          
              def getParameter(self, param):
                  return getattr(self, param)
          
          class TestMyDevice(unittest.TestCase):
          
              def setUp(self):
                  self.dev1 = MyDevice()
          
              def tearDown(self):
                  del self.dev1
          
              def test_name(self):
                  """ Test for valid input for name parameter """
          
                  self.dev1.setParameter('name', 'MyDevice')
                  name = self.dev1.getParameter('name')
                  self.assertEqual(name, 'MyDevice')
          
              def test_invalid_name(self):
                  """ Test to check if error is raised if invalid type of input is provided """
          
                  self.assertRaises(DeviceException, self.dev1.setParameter, 'name', 1234)
          
              def test_exception_message(self):
                  """ Test to check if correct exception message and code is raised when incorrect value is passed """
          
                  with self.assertRaises(DeviceException) as cm:
                      self.dev1.setParameter('name', 1234)
                  self.assertEqual(cm.exception.msg, 'Incorrect type of argument passed. Name expects a string', 'mismatch in expected error message')
                  self.assertEqual(cm.exception.code, 100001, 'mismatch in expected error code')
          
          
          if __name__ == '__main__':
              unittest.main()
          

          【讨论】:

          • 最后一次测试test_exception_message 真的很有帮助。需要获取返回的异常消息。谢谢你
          【解决方案8】:

          虽然所有答案都很好,但我一直在寻找一种方法来测试函数是否引发异常,而无需依赖单元测试框架并且不必编写测试类。

          我最终写了以下内容:

          def assert_error(e, x):
              try:
                  e(x)
              except:
                  return
              raise AssertionError()
          
          def failing_function(x):
              raise ValueError()
          
          def dummy_function(x):
              return x
          
          if __name__=="__main__":
              assert_error(failing_function, 0)
              assert_error(dummy_function, 0)
          

          它在正确的行上失败了:

          Traceback (most recent call last):
            File "assert_error.py", line 16, in <module>
              assert_error(dummy_function, 0)
            File "assert_error.py", line 6, in assert_error
              raise AssertionError()
          AssertionError
          

          【讨论】:

            【解决方案9】:

            您的代码应遵循此模式(这是一个单元测试模块样式测试):

            def test_afunction_throws_exception(self):
                try:
                    afunction()
                except ExpectedException:
                    pass
                except Exception:
                   self.fail('unexpected exception raised')
                else:
                   self.fail('ExpectedException not raised')
            

            在 Python assertRaises 只检查是否引发了异常。

            【讨论】:

            • 和方法 self.fail 只接受一个参数
            • 这对于测试函数是否抛出异常似乎过于复杂。由于除该异常之外的任何异常都会导致测试出错并且不引发异常将使测试失败,似乎唯一的区别是,如果您使用assertRaises 获得不同的异常,您将获得错误而不是失败。
            【解决方案10】:

            从 Python 2.7 开始,您可以使用上下文管理器来获取实际抛出的异常对象:

            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

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

            【讨论】:

            • 我用的是Python 2.7.10,上面的不行; context.exception不给消息;它是一种类型。
            • 同样在Python 2.7(至少在我的2.7.6)中使用import unittest2,你需要使用str(),即self.assertTrue('This is broken' in str(context.exception))
            • 两件事:1.您可以使用assertIn代替assertTrue。例如。 self.assertIn('This is broken', context.exception) 2. 在我的情况下,使用 2.7.10,context.exception 似乎是一个字符数组。使用 str 不起作用。我最终这样做了:''.join(context.exception) 所以,放在一起:self.assertIn('This is broken', ''.join(context.exception))
            • 您的方法使用异常的 Traceback 阻塞测试控制台是否正常?如何防止这种情况发生?
            • 后来我找到了另一种获取消息作为异常str的方法,它是err = context.exception.message。然后也可以使用 self.assertEqual(err, 'This is broken') 来做测试。
            【解决方案11】:

            使用单元测试模块中的TestCase.assertRaises(或TestCase.failUnlessRaises),例如:

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

            【讨论】:

            • 有没有办法做相反的事情?只有当函数抛出异常时它才会失败?
            • 请注意,要将参数传递给myfunc,您需要将它们作为参数添加到 assertRaises 调用中。请参阅达里尔·斯皮策的回答。
            • 有没有办法允许多种异常类型?
            • 测试类构造函数也一样:self.assertRaises(SomeCoolException, Constructor, arg1)
            • 另一种传递参数的方式是self.assertRaises(MyException, lambda: do_something(with=some, arguments))
            【解决方案12】:

            如何测试 Python 函数是否引发异常?

            如何编写仅在函数不抛出时才失败的测试 预期的异常?

            简答:

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

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

            演示

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

            unittest

            在 Python 2.7 或 3 中:

            import unittest
            

            在 Python 2.6 中,您可以安装 2.7 的 unittest 库的反向移植,称为 unittest2,并将其别名为 unittest

            import unittest2 as unittest
            

            示例测试

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

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

            测试一使用assertRaises 作为上下文管理器,确保在记录时正确捕获和清理错误。

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

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

            运行测试

            运行测试:

            unittest.main(exit=False)
            

            在 Python 2.6 中,您可能会need the following

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

            您的终端应输出以下内容:

            ..
            ----------------------------------------------------------------------
            Ran 2 tests in 0.007s
            
            OK
            <unittest2.runner.TextTestResult run=2 errors=0 failures=0>
            

            我们看到,正如我们所料,尝试添加 1'1' 会导致 TypeError


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

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

            【讨论】:

              【解决方案13】:

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

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

              如果一个函数接受参数,只需像这样将它们传递给 assertRaises:

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

              【讨论】:

              • 第二个关于参数传递时要做什么的片段真的很有帮助。
              • 我正在使用2.7.15。如果self.assertRaises(ExpectedException, afunction, arg1, arg2) 中的afunction 是类初始化器,则需要将self 作为第一个参数传递,例如self.assertRaises(ExpectedException, Class, self, arg1, arg2)
              • 如果 arg 必须是 parameter=value 类型,它也可以工作,例如:self.assertRaises(ExpectedException, afunction, arg1=val1)
              【解决方案14】:

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

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

              然后你可以像这样使用raises

              with raises(Exception):
                  print "Hola"  # Calls assert False
              
              with raises(Exception):
                  raise Exception  # Calls assert True
              

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

              例子:

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

              结果:

              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
              

              【讨论】:

              • 感谢您发布不需要unittest 模块的答案!
              【解决方案15】:

              您可以使用 unittest 模块中的 assertRaises

              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)
              

              【讨论】:

                【解决方案16】:

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

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

                【讨论】:

                【解决方案17】:

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

                看看这段代码:

                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 documentation: 23.2 doctest -- Test interactive Python examples

                【讨论】:

                • 我喜欢 doctest,但我发现它是对 unittest 的补充而不是替代。
                • doctest 在自动重构方面不太可能发挥作用吗?我想为python设计的重构工具应该了解文档字符串。任何人都可以根据他们的经验发表评论吗?
                猜你喜欢
                • 2018-01-16
                • 2018-06-16
                • 1970-01-01
                • 1970-01-01
                • 2020-11-05
                • 2017-05-16
                • 2019-11-26
                相关资源
                最近更新 更多