Python pytest pytest_exception_interact从VCR.py异常中自定义异常信息

时间:2019-06-25 01:52:11

标签: python python-3.x pytest

上下文

我已经开始使用pytest-vcr,它是pytest的一个VCR.py插件,包装在this blog post on Advanced Python Testing中。

它在第一次测试运行时将所有HTTP流量记录到cassettes/*.yml文件中以保存快照。类似于针对Web组件的Jest快照测试。

在随后的测试运行中,如果请求格式错误,它将找不到匹配项,并抛出异常,表明禁止记录新请求,并且找不到现有记录。

>

问题

VCR.py引发了一个CannotOverwriteExistingCassetteException,它为什么不匹配的信息不多。

  

我该如何利用pytest pytest_exception_interact钩子将这一异常替换为具有更多信息的,利用固定装置信息?

我深入到site-packagesVCR.py的{​​{1}}中,并重写了我希望它如何处理异常。我只需要知道如何使此pip installed钩子正常工作即可从该测试节点访问固定装置(在清理之前)并引发另一个异常。

示例

让我们获取依赖关系。

pytest_exception_interact

test_example.py:

$ pip install pytest pytest-vcr requests
import pytest
import requests

@pytest.mark.vcr
def test_example():
    r = requests.get("https://www.stackoverflow.com")
    assert r.status_code == 200

现在将测试中的URI更改为“ https://www.google.com”:

test_example.py:

$ pytest test_example.py --vcr-record=once
...
test_example.py::test_example PASSED 
...
$ ls cassettes/
cassettes/test_example.yml
$ head cassettes/test_example.yml
interactions:
- request:
    uri: https://wwwstackoverflow.com
    body: null
    headers:
        Accept:
        - '*/*'
$ pytest test_example.py --vcr-record=none
...
test_example.py::test_example PASSED 
...

然后再次运行测试以检测回归:

import pytest
import requests

@pytest.mark.vcr
def test_example():
    r = requests.get("https://www.google.com")
    assert r.status_code == 200

我可以在测试结构的根目录中添加一个$ pytest test_example.py --vcr-record=none E vcr.errors.CannotOverwriteExistingCassetteException: No match for the request (<Request (GET) https://www.google.com/>) ... 文件,以创建一个本地插件,并且可以验证是否可以拦截该异常并使用以下命令注入自己的内容:< / p>

conftest.py

conftest.py

这是互联网上的文档变得很薄的部分。

  

如何访问vcr_cassette固定装置并返回其他异常?

我想做的是获取试图被请求的import pytest from vcr.errors import CannotOverwriteExistingCassetteException from vcr.config import VCR from vcr.cassette import Cassette class RequestNotFoundCassetteException(CannotOverwriteExistingCassetteException): ... @pytest.fixture(autouse=True) def _vcr_marker(request): marker = request.node.get_closest_marker("vcr") if marker: cassette = request.getfixturevalue("vcr_cassette") vcr = request.getfixturevalue("vcr") request.node.__vcr_fixtures = dict(vcr_cassette=cassette, vcr=vcr) yield @pytest.hookimpl(hookwrapper=True) def pytest_exception_interact(node, call, report): excinfo = call.excinfo if report.when == "call" and isinstance(excinfo.value, CannotOverwriteExistingCassetteException): # Safely check for fixture pass through on this node cassette = None vcr = None if hasattr(node, "__vcr_fixtures"): for fixture_name, fx in node.__vcr_fixtures.items(): vcr = fx if isinstance(fx, VCR) cassette = fx if isinstance(fx, Cassette) # If we have the extra fixture context available... if cassette and vcr: match_properties = [f.__name__ for f in cassette._match_on] cassette_reqs = cassette.requests # filtered_req = cassette.filter_request(vcr._vcr_request) # this_req, req_str = __format_near_match(filtered_req, cassette_reqs, match_properties) # Raise and catch a new excpetion FROM existing one to keep the traceback # https://stackoverflow.com/a/24752607/622276 # https://docs.python.org/3/library/exceptions.html#built-in-exceptions try: raise RequestNotFoundCassetteException( f"\nMatching Properties: {match_properties}\n" f"Cassette Requests: {cassette_reqs}\n" ) from excinfo.value except RequestNotFoundCassetteException as e: excinfo._excinfo = (type(e), e) report.longrepr = node.repr_failure(excinfo) ,并且filtered_request的列表并使用Python difflib标准库产生针对差异信息的增量。 / p>

PyTest代码拼写

使用pytest触发器cassette_requests运行单个测试的内部机制,该触发器有效地运行以下三个pytest_runtest_protocol调用以获取报告集合。

src/_pytest/runner.py:L77-L94

call_and_report

因此,我在 call 阶段修改了报告之后……但是仍然不知道如何访问灯具信息。

src/_pytest/runner.py:L166-L174

def runtestprotocol(item, log=True, nextitem=None):
    # Abbreviated
    reports = []
    reports.append(call_and_report(item, "setup", log))
    reports.append(call_and_report(item, "call", log))
    reports.append(call_and_report(item, "teardown", log))
    return reports

似乎有一些用于生成新ExceptionRepresentation的辅助方法,因此我更新了 conftest.py 示例。

src/_pytest/reports.py:L361

def call_and_report(item, when, log=True, **kwds):
    call = call_runtest_hook(item, when, **kwds)
    hook = item.ihook
    report = hook.pytest_runtest_makereport(item=item, call=call)
    if log:
        hook.pytest_runtest_logreport(report=report)
    if check_interactive_exception(call, report):
        hook.pytest_exception_interact(node=item, call=call, report=report)
    return report

更新#1 2019-06-26 :多亏了some pointers from @hoefling,我更新了 conftest.py

  • 正确使用raise ... from ...表单重新引发异常。
  • 覆盖longrepr = item.repr_failure(excinfo) ,将_vcr_markervcr固定装置附加到代表单个测试项目的vcr_cassette上。
  • 剩余:从已打补丁的VCRConnection中访问已拦截的请求...

更新#2 2019-06-26

获得在创建盒带上下文管理器时打补丁的VCRHTTPConnections似乎是不可能的。我打开了以下拉取请求,在引发异常时将其作为参数传递,然后任意捕获和处理下游数据。

https://github.com/kevin1024/vcrpy/pull/445

相关

有意义的相关问题,但仍然无法回答该问题。

1 个答案:

答案 0 :(得分:0)

感谢@hoefling的评论中的评论和指导。

我可以将cassette固定装置附加到request.node本地插件中的conftest.py上,以覆盖pytest-vcr标记...

@pytest.fixture(autouse=True)
def _vcr_marker(request):
    marker = request.node.get_closest_marker("vcr")
    if marker:
        cassette = request.getfixturevalue("vcr_cassette")
        vcr = request.getfixturevalue("vcr")
        request.node.__vcr_fixtures = dict(vcr_cassette=cassette, vcr=vcr)
    yield

但是我需要的比磁带还要多。

成分

食谱

获取最新的VCRpy

在撰写本文时(2019-07-01)合并的pull请求尚未发布到PyPI,因此使用从github链接安装pip的此选项将获得预发行版本:

pip install from git repo branch

pip install https://github.com/kevin1024/vcrpy/archives/master.zip

覆盖pytest_exception_interact钩子

在测试目录的根目录中,创建一个conftest.pycreate a local plugin,以覆盖pytest_exception_interact hook

@pytest.hookimpl(hookwrapper=True)
def pytest_exception_interact(node, call, report):
    """Intercept specific exceptions from tests."""
    if report.when == "call" and isinstance(call.excinfo.value, CannotOverwriteExistingCassetteException):
        __handle_cassette_exception(node, call, report)

    yield

从异常中提取CassetteRequest

# Define new exception to throw
class RequestNotFoundCassetteException(Exception):
   ...

def __handle_cassette_exception(node, call, report):
    # Safely check for attributes attached to exception
    vcr_request = None
    cassette = None
    if hasattr(call.excinfo.value, "cassette"):
        cassette = call.excinfo.value.cassette
    if hasattr(call.excinfo.value, "failed_request"):
        vcr_request = call.excinfo.value.failed_request

    # If we have the extra context available...
    if cassette and vcr_request:

        match_properties = [f.__name__ for f in cassette._match_on]
        this_req, req_str = __format_near_match(cassette.requests, vcr_request, match_properties)

        try:
            raise RequestNotFoundCassetteException(f"{this_req}\n\n{req_str}\n") from call.excinfo.value
        except RequestNotFoundCassetteException as e:
            call.excinfo._excinfo = (type(e), e)
            report.longrepr = node.repr_failure(call.excinfo)

相关问题