如何为py.test创建扩展的xfail标记?

时间:2019-07-01 08:16:18

标签: python unit-testing testing pytest

py.test(版本4.6.2)中,您有一个标记修饰符来进行测试以将其标记为失败,例如

@pytest.mark.xfail
def test1():
    return 1/0

还可以验证异常本身

@pytest.mark.xfail(raises=ZeroDivisionError)

但是可以通过某种方式验证错误消息本身吗?

当您有HTTPError时,这很有用,因为可能有很多原因。而且,当您比较错误消息本身时,可以更详细地说明测试何时失败(例如,将某个Client ErrorServer Error区别开)。

到目前为止,我正在使用以下构造:

def test_fail_request(self):      
    with pytest.raises(requests.exceptions.HTTPError) as excinfo:
        response = requests.something
    assert '403 Client Error: Not Found' in str(excinfo.value)

但是,py.test当然可以进行如下所示的测试,使其更具可读性,紧凑性和正确性:

 @pytest.mark.xfail(expected_error = "403 Client Error: Not Found"):
 def test_fail_request(self):      
    response = requests.something

有没有办法实现这种行为/功能?

为澄清起见,最后一个代码示例应该会失败,但仅在错误消息包含特定消息时才失败(示例:400 Client Error: Bad Request)。在这种情况下,测试将报告为XFAIL

但是,如果测试失败并创建一条不同错误消息(即使是相同的异常,例如错误消息中的500 Server Error),则该测试必须报告为“意外传递”(XPASS)。

2 个答案:

答案 0 :(得分:2)

如果失败是正常现象,则可以创建自己的装饰器,例如:

import functools
def expect_http_error(func=None, *, expected_error):
  def wrapper(func):
    @functools.wraps(func)
    def inner(*args, **kwargs):
      with pytest.raises(requests.exceptions.HTTPError) as excinfo:
        func(*args, **kwargs)
      assert expected_error in str(excinfo.value)

    return inner

  return wrapper if func is None else wrapper(func)

然后像这样使用它:

@expect_http_error(expected_error = "403 Client Error: Not Found")
def test_fail_request(self):      
  response = requests.something

答案 1 :(得分:0)

在标记中存储自定义数据

pytest标记接受任意参数,因此使用自定义参数扩展内置标记非常容易:

@pytest.mark.xfail(raises=ZeroDivisionError, match='.* by zero')
def test():
    return 1 / 0

尽管未为match标记定义xfail自变量,但是它将被存储,但是默认情况下保持未使用状态。然后,您可以在自定义hookimpls中访问该参数。

在挂钩中使用自定义标记数据

警告

  

但是,如果测试失败并创建一条不同错误消息(即使是相同的异常,例如错误消息中的500 Server Error),则该测试必须报告为“意外传递”(XPASS)。

请注意,pytest如果引发意外类型的异常,则测试失败,例如:

@pytest.mark.xfail(raises=RuntimeError)
def test():
    return 1 / 0

将报告为失败:

============================================== FAILURES ==============================================
_______________________________________________ test ________________________________________________

    @pytest.mark.xfail(raises=RuntimeError)
    def test():
>       return 1 / 0
E       ZeroDivisionError: division by zero

test_spam.py:18: ZeroDivisionError

请注意,由于您实际上想要xpass测试会引发意外异常,因此该测试与pytest标准行为有所不同。因此,我实现了以下钩子,如下所示:

  • 如果测试带有“标准” xfail标记(不带match参数),例如

    @pytest.mark.xfail(raises=Foo)
    

    如果引发Foo,则测试将失败,否则将失败(pytest标准行为)

  • 如果测试带有xfail参数的match标记,例如

    @pytest.mark.xfail(raises=Foo, match='.*bar.*')
    

    在所有其他情况下(例如,以不同的消息引发xfail的情况下,如果引发Foo且匹配.*bar.*的正则表达式的邮件,将 Foo ,引发Bar等),它将xpass

hookimpl:

# conftest.py

import re
from pytest import hookimpl


@hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
    outcome = yield
    rep = outcome.get_result()
    if not hasattr(item, '_evalxfail'):
        return  # no xfail marker

    evalxfail = item._evalxfail
    match_expr = evalxfail.get('match')
    if match_expr is None:
        return  # no match argument in xfail marker

    if call.excinfo and evalxfail.wasvalid() and evalxfail.istrue():
        match = re.search(match_expr, str(call.excinfo.value))
        # if the exception doesn't match the type or description, set to XPASS 
        if match or evalxfail.invalidraise(call.excinfo.value):
            rep.outcome = 'passed'
            rep.wasxfail = evalxfail.getexplanation()

要进行进一步的自定义,请更改挂钩中的条件检查。例如,如果您想:

  • xfail(如果异常的类型正确且与消息正则表达式匹配)
  • 如果异常的类型错误,则失败
  • xpass,如果异常的类型正确,但与正则表达式不匹配

换行

if match or evalxfail.invalidraise(call.excinfo.value):

if match: