【发布时间】:2019-11-06 19:22:41
【问题描述】:
上下文
我已经开始使用pytest-vcr,这是一个包装VCR.py 的pytest 插件,我在this blog post on Advanced Python Testing 中记录了它。
它会在第一次测试运行时将所有 HTTP 流量记录到 cassettes/*.yml 文件以保存快照。类似于Jest web 组件的快照测试。
在随后的测试运行中,如果请求格式不正确,它将找不到匹配项并抛出异常,指出禁止记录新请求并且它没有找到现有记录。 p>
问题
VCR.py 提出了一个 CannotOverwriteExistingCassetteException,它并没有特别说明为什么它不匹配。
如何利用 pytest
pytest_exception_interact钩子将这个异常替换为利用夹具信息提供更多信息的异常?
我深入我的site-packages,其中VCR.py 是pip 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_request 和cassette_requests 的列表,并使用Python difflib 标准库生成针对分歧信息的增量。
PyTest 代码探索
使用 pytest 运行单个测试的内部触发 pytest_runtest_protocol 有效地运行以下三个 call_and_report 调用以获取报告集合。
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 示例。
longrepr = item.repr_failure(excinfo)
UPDATE #1 2019-06-26:感谢 cmets 中的 some pointers from @hoefling 我更新了我的 conftest.py。
-
正确使用
raise ... from ...表单重新引发异常。 - 覆盖
_vcr_marker以将vcr和vcr_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