【问题标题】:Python pytest pytest_exception_interact customize exception information from VCR.py exceptionPython pytest pytest_exception_interact 从 VCR.py 异常自定义异常信息
【发布时间】:2019-11-06 19:22:41
【问题描述】:

上下文

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

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

在随后的测试运行中,如果请求格式不正确,它将找不到匹配项并抛出异常,指出禁止记录新请求并且它没有找到现有记录。 p>

问题

VCR.py 提出了一个 CannotOverwriteExistingCassetteException,它并没有特别说明为什么它不匹配。

如何利用 pytest pytest_exception_interact 钩子将这个异常替换为利用夹具信息提供更多信息的异常?

我深入我的site-packages,其中VCR.pypip installed,并重写了我希望它如何处理异常。我只需要知道如何让这个pytest_exception_interact 挂钩正常工作,以便从该测试节点访问固定装置(在它被清理之前)并引发不同的异常。

示例

让我们获取依赖项。

$ pip install pytest pytest-vcr requests

test_example.py:

import pytest
import requests

@pytest.mark.vcr
def test_example():
    r = requests.get("https://www.stackoverflow.com")
    assert r.status_code == 200
$ 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 
...

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

test_example.py:

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/>)
...

我可以将conftest.py 文件添加到我的测试结构的根目录以创建一个本地插件,并且我可以验证我可以拦截异常并注入我自己的使用:

conftest.py

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)


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

如何访问vcr_cassette 夹具并返回不同的异常?

我想要做的是获取试图被请求的filtered_requestcassette_requests 的列表,并使用Python difflib 标准库生成针对分歧信息的增量。

PyTest 代码探索

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

src/_pytest/runner.py:L77-L94

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

所以我在 call 阶段修改了报告...但仍然不知道如何访问灯具信息。

src/_pytest/runner.py:L166-L174

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

看起来有一些辅助方法可以生成新的 ExceptionRepresentation,所以我更新了 conftest.py 示例。

src/_pytest/reports.py:L361

longrepr = item.repr_failure(excinfo)

UPDATE #1 2019-06-26:感谢 cmets 中的 some pointers from @hoefling 我更新了我的 conftest.py

  • 正确使用raise ... from ... 表单重新引发异常。
  • 覆盖 _vcr_marker 以将 vcrvcr_cassette 固定装置附加到代表单个测试项的 request.node
  • Remaining:从修补后的 VCRConnection 获取拦截的请求...

更新 #2 2019-06-26

在创建磁带上下文管理器时修补的 VCRHTTPConnections 似乎是不可能的。我已经打开了以下拉取请求,以便在抛出异常时作为参数传递,然后在下游任意捕获和处理。

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

相关

提供信息但仍未回答此问题的相关问题。

【问题讨论】:

  • 我希望所有关于 SO 的问题都是这样的。
  • 关于您的实际问题:您可以通过将夹具值附加到全局对象来传递它们,通常我为此使用config。例如。用主体request.config._vcr = vcr; return vcr 重新定义vcr 夹具,并通过挂钩中的node.session.config._vcr 访问它。如果您需要,我可以用一个工作示例在答案中描述它。
  • 关于异常修改:pytest_exception_interact 钩子在正确的轨道上;但是,我不会修改现有的 ExceptionInfo 对象,而是创建一个新对象。我不喜欢你的方法是交换的异常不再匹配回溯;我可能会从钩子中的当前异常 (raise MyEx('text') from call.excinfo.value) 中引发您自己类型的新异常,捕获它并通过例如注入新的 excinfo。 call.excinfo = ExceptionInfo.from_current(),所以至少你保留了原始回溯。
  • 谢谢@hoefling!明天我会试一试,然后转回来。我设法拦截了异常并将其替换为新异常。这可能是获取新的详细异常所需的夹具上下文的技巧。哦,关于提出新异常的好技巧。无论如何,我不喜欢替换。
  • 我不确定提升和立即捕获是否是一个好方法,我可以想象它在代码中看起来很丑。我会考虑更好的事情并报告回来。

标签: python python-3.x pytest


【解决方案1】:

感谢 cmets 和来自 @hoefling 的 cmets 的指导。

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

@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

​​>

这些补丁发布于vcrpy v2.1.0

pip install vcrpy==2.1.0

覆盖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)

【讨论】:

    猜你喜欢
    • 2011-01-09
    • 1970-01-01
    • 1970-01-01
    • 2010-09-08
    • 1970-01-01
    • 2014-02-04
    • 2015-11-04
    • 2012-01-17
    • 1970-01-01
    相关资源
    最近更新 更多