【问题标题】:How to properly assert that an exception gets raised in pytest?如何正确断言在 pytest 中引发了异常?
【发布时间】:2014-06-13 18:51:50
【问题描述】:

代码:

# coding=utf-8
import pytest


def whatever():
    return 9/0

def test_whatever():
    try:
        whatever()
    except ZeroDivisionError as exc:
        pytest.fail(exc, pytrace=True)

输出:

================================ test session starts =================================
platform linux2 -- Python 2.7.3 -- py-1.4.20 -- pytest-2.5.2
plugins: django, cov
collected 1 items 

pytest_test.py F

====================================== FAILURES ======================================
___________________________________ test_whatever ____________________________________

    def test_whatever():
        try:
            whatever()
        except ZeroDivisionError as exc:
>           pytest.fail(exc, pytrace=True)
E           Failed: integer division or modulo by zero

pytest_test.py:12: Failed
============================== 1 failed in 1.16 seconds ==============================

如何使 pytest 打印回溯,以便我查看whatever 函数中的哪个位置引发了异常?

【问题讨论】:

  • 我得到了整个回溯,Ubuntu 14.04,Python 2.7.6
  • @thefourtheye 请提供输出要点。我尝试使用 Python 2.7.4 和 Ubuntu 14.04 - 结果与我在主帖中描述的相同。
  • @GillBates 一会儿我读到“比尔盖茨”..

标签: python unit-testing testing pytest


【解决方案1】:

pytest 不断发展,随着最近的一个不错的变化,现在可以同时测试

  • 异常类型(严格测试)
  • 错误消息(使用正则表达式进行严格或松散检查)

文档中的两个示例:

with pytest.raises(ValueError, match='must be 0 or None'):
    raise ValueError('value must be 0 or None')
with pytest.raises(ValueError, match=r'must be \d+$'):
    raise ValueError('value must be 42')

我已经在许多项目中使用了这种方法,并且非常喜欢它。

注意: This comment by ilya-rusin 也建议使用上述方法。

【讨论】:

  • 这应该是公认的答案。它是最新的和“Pythonic”,这意味着它简洁、易读且易于理解。
【解决方案2】:

如果您希望为您的测试用例引发异常,则此处提交的最佳答案很有用。如果您的测试可能引发异常并且您希望在任何一种情况下都优雅地处理它,那么这不是很有用。

如果您有一个可能(不会)引发异常的测试用例,我认为这可能是一个更好的选择。

@python.mark.parametrize("request_query, response_code", query_response_dataset)
def test_big_query_submission(request_query, response_code):
    try:
        stats = bigquery.Client().query(request_query)
    except Exception as e:
        assert False, f"Exception raised: {e}"
    assert stats.errors is None

通过这种方式,您可以优雅地失败测试,​​而不是由于任何原因引发的异常导致测试崩溃。

【讨论】:

    【解决方案3】:

    如果您想测试特定的错误类型,请结合使用 try、catch 和 raise:

    #-- test for TypeError
    try:
      myList.append_number("a")
      assert False
    except TypeError: pass
    except: assert False
    

    【讨论】:

      【解决方案4】:

      在pytest中有两种处理这种情况的方法:

      • 使用pytest.raises函数

      • 使用pytest.mark.xfail装饰器

      正如documentation 所说:

      使用pytest.raises 可能更适合您测试自己的代码故意引发的异常的情况,而使用带有检查功能的@pytest.mark.xfail 可能更适合记录未修复的错误(其中测试描述了什么“应该”发生)或依赖项中的错误。

      pytest.raises的用法:

      def whatever():
          return 9/0
      def test_whatever():
          with pytest.raises(ZeroDivisionError):
              whatever()
      

      pytest.mark.xfail的用法:

      @pytest.mark.xfail(raises=ZeroDivisionError)
      def test_whatever():
          whatever()
      

      pytest.raises 的输出:

      ============================= test session starts ============================
      platform linux2 -- Python 2.7.10, pytest-3.2.3, py-1.4.34, pluggy-0.4.0 -- 
      /usr/local/python_2.7_10/bin/python
      cachedir: .cache
      rootdir: /home/user, inifile:
      collected 1 item
      
      test_fun.py::test_whatever PASSED
      
      
      ======================== 1 passed in 0.01 seconds =============================
      

      pytest.xfail 标记的输出:

      ============================= test session starts ============================
      platform linux2 -- Python 2.7.10, pytest-3.2.3, py-1.4.34, pluggy-0.4.0 -- 
      /usr/local/python_2.7_10/bin/python
      cachedir: .cache
      rootdir: /home/user, inifile:
      collected 1 item
      
      test_fun.py::test_whatever xfail
      
      ======================== 1 xfailed in 0.03 seconds=============================
      

      【讨论】:

      • xfail 不是这里问题的解决方案,它只是让测试失败。在这里,我们想检查是否引发了某个异常。
      • 我必须重申@Ctrl-C 的评论:pytest.mark.xfail 不会断言引发了异常,它只是允许引发异常。这不是问题的标题。
      • 按原样,这个答案具有误导性。解释 xfail 的文档段落应该移到顶部。
      【解决方案5】:

      pytest中的异常有两种处理方式:

      1. 使用pytest.raises 编写有关引发异常的断言
      2. 使用@pytest.mark.xfail

      1。使用pytest.raises

      来自the docs

      为了编写有关引发异常的断言,您可以使用pytest.raises 作为上下文管理器

      示例:

      断言只是一个例外:

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

      with pytest.raises(ZeroDivisionError) 说不管是什么 在下一个代码块中应该引发ZeroDivisionError 异常。如果没有引发异常,则测试失败。如果测试引发不同的异常,则会失败。

      如果您需要访问实际的异常信息:

      import pytest
      
      def f():
          f()
      
      def test_recursion_depth():
          with pytest.raises(RuntimeError) as excinfo:
              f()
          assert "maximum recursion" in str(excinfo.value)
      

      excinfo 是一个 ExceptionInfo 实例,它是引发的实际异常的包装器。感兴趣的主要属性是.type.value.traceback

      2。使用@pytest.mark.xfail

      也可以为pytest.mark.xfail 指定一个raises 参数。

      import pytest
      
      @pytest.mark.xfail(raises=IndexError)
      def test_f():
          l = [1, 2, 3]
          l[10]
      

      @pytest.mark.xfail(raises=IndexError) 说不管是什么 在下一个代码块中应该引发IndexError 异常。如果引发IndexError,则测试标记为xfailed (x)。如果没有引发异常,则测试标记为xpassed (X)。如果测试引发不同的异常,则会失败。

      注意事项:

      • 使用pytest.raises 可能更适合您测试自己的代码故意引发的异常,而使用带有检查功能的@pytest.mark.xfail 可能更适合记录未修复的错误或依赖项中的错误。

      • 您可以将 match 关键字参数传递给上下文管理器 (pytest.raises) 以测试正则表达式是否匹配异常的字符串表示。 (see more)

      【讨论】:

        【解决方案6】:

        pytest.raises(Exception) 是您所需要的。

        代码

        import pytest
        
        def test_passes():
            with pytest.raises(Exception) as e_info:
                x = 1 / 0
        
        def test_passes_without_info():
            with pytest.raises(Exception):
                x = 1 / 0
        
        def test_fails():
            with pytest.raises(Exception) as e_info:
                x = 1 / 1
        
        def test_fails_without_info():
            with pytest.raises(Exception):
                x = 1 / 1
        
        # Don't do this. Assertions are caught as exceptions.
        def test_passes_but_should_not():
            try:
                x = 1 / 1
                assert False
            except Exception:
                assert True
        
        # Even if the appropriate exception is caught, it is bad style,
        # because the test result is less informative
        # than it would be with pytest.raises(e)
        # (it just says pass or fail.)
        
        def test_passes_but_bad_style():
            try:
                x = 1 / 0
                assert False
            except ZeroDivisionError:
                assert True
        
        def test_fails_but_bad_style():
            try:
                x = 1 / 1
                assert False
            except ZeroDivisionError:
                assert True
        

        输出

        ============================================================================================= test session starts ==============================================================================================
        platform linux2 -- Python 2.7.6 -- py-1.4.26 -- pytest-2.6.4
        collected 7 items 
        
        test.py ..FF..F
        
        =================================================================================================== FAILURES ===================================================================================================
        __________________________________________________________________________________________________ test_fails __________________________________________________________________________________________________
        
            def test_fails():
                with pytest.raises(Exception) as e_info:
        >           x = 1 / 1
        E           Failed: DID NOT RAISE
        
        test.py:13: Failed
        ___________________________________________________________________________________________ test_fails_without_info ____________________________________________________________________________________________
        
            def test_fails_without_info():
                with pytest.raises(Exception):
        >           x = 1 / 1
        E           Failed: DID NOT RAISE
        
        test.py:17: Failed
        ___________________________________________________________________________________________ test_fails_but_bad_style ___________________________________________________________________________________________
        
            def test_fails_but_bad_style():
                try:
                    x = 1 / 1
        >           assert False
        E           assert False
        
        test.py:43: AssertionError
        ====================================================================================== 3 failed, 4 passed in 0.02 seconds ======================================================================================
        

        请注意,e_info 保存异常对象,以便您可以从中提取详细信息。例如,如果要检查异常调用堆栈或内部的另一个嵌套异常。

        【讨论】:

        • 如果您可以包含一个实际查询e_info 的示例,那就太好了。对于更熟悉某些其他语言的开发人员来说,e_info 的范围延伸到 with 块之外并不明显。
        • 如果您期望为您的测试引发异常,这很有用。如果您的测试可能引发异常并且您想优雅地处理它,这不是很有用。
        • 对于严格或松散的错误消息检查(除了检查异常类型),请使用 match 关键字参数 - 另请参阅此答案:stackoverflow.com/a/56569533/145400
        【解决方案7】:

        你的意思是这样的:

        def test_raises():
            with pytest.raises(Exception) as execinfo:   
                raise Exception('some info')
            # these asserts are identical; you can use either one   
            assert execinfo.value.args[0] == 'some info'
            assert str(execinfo.value) == 'some info'
        

        【讨论】:

        • assert excinfo.match(r"^some info$") 也可以
        • 自版本3.1 起,您可以使用关键字参数match 断言异常匹配文本或正则表达式:with raises(ValueError, match='must be 0 or None'): raise ValueError("value must be 0 or None")with raises(ValueError, match=r'must be \d+$'): raise ValueError("value must be 42")
        • 重要提示:如果在没有消息的情况下引发异常,execinfo.value.args 将是一个空元组,这意味着尝试执行 execinfo.value.args[0] 将引发异常。更安全message = execinfo.value.args[0] if execinfo.value.args else None; assert message == 'some info'.
        【解决方案8】:

        我们正在使用这个解决方案:

        def test_date_invalidformat():
            """
            Test if input incorrect data will raises ValueError exception
            """
            date = "06/21/2018 00:00:00"
            with pytest.raises(ValueError):
                app.func(date) #my function to be tested
        

        请参考pytest,https://docs.pytest.org/en/latest/reference.html#pytest-raises

        【讨论】:

          【解决方案9】:

          正确的方法是使用pytest.raises,但我在 cmets here 中发现了有趣的替代方法,并希望将其保存以供未来读者阅读:

          try:
              thing_that_rasises_typeerror()
              assert False
          except TypeError:
              assert True
          

          【讨论】:

          • 这是不好的风格。如果“事物”没有引发错误,或引发意外错误,则您会在没有任何测试报告或分析上下文的情况下得到 assert False。 pytest traceback 在断言处停止,目前尚不清楚要测试的内容。甚至最初的评论也提到有更好的方法,但“只是为了展示编写测试的速度有多快”。为什么在没有上下文的情况下“值得保存”?
          【解决方案10】:

          你可以试试

          def test_exception():
              with pytest.raises(Exception) as excinfo:   
                  function_that_raises_exception()   
              assert str(excinfo.value) == 'some info' 
          

          【讨论】:

          • 要在 pytest 5.0.0 中将异常消息/值作为字符串获取,需要使用 str(excinfo.value)。它也适用于 pytest 4.x。在 pytest 4.x 中,str(excinfo) 也有效,但 not 在 pytest 5.0.0 中有效。
          【解决方案11】:

          更好的做法是使用继承 unittest.TestCase 并运行 self.assertRaises 的类。

          例如:

          import unittest
          
          
          def whatever():
              return 9/0
          
          
          class TestWhatEver(unittest.TestCase):
          
              def test_whatever():
                  with self.assertRaises(ZeroDivisionError):
                      whatever()
          

          然后你可以通过运行来执行它:

          pytest -vs test_path
          

          【讨论】:

          • 比什么更好的实践?我根本不会调用使用 unittest 语法而不是 pytest 语法“更好的做法”。
          • 它可能不是“更好”,但它是一个有用的选择。因为答案的标准是实用性,所以我赞成。
          • 看起来pytestnosex 更受欢迎,但这就是我使用pytest 的方式。
          【解决方案12】:

          您是否尝试过删除“pytrace=True”?

          pytest.fail(exc, pytrace=True) # before
          pytest.fail(exc) # after
          

          您是否尝试过使用 '--fulltrace' 运行?

          【讨论】:

            猜你喜欢
            • 2023-03-03
            • 2020-04-20
            • 2019-01-28
            • 2017-07-27
            • 2020-10-15
            • 2010-09-14
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多