Python pytest pytest_exception_interact настроить информацию об исключении из исключения VCR.py - PullRequest
2 голосов
/ 25 июня 2019

Контекст

Я начал использовать pytest-vcr, который является pytest оберткой плагина VCR.py, который я задокументировал в этой записи блога по расширенному тестированию Python .

Записывает весь HTTP-трафик в cassettes/*.yml файлы при первом запуске теста для сохранения снимков. Аналогично Jest тестирование снимков для веб-компонентов.

В последующих тестовых прогонах, если запрос искажен, он не найдет совпадение и выдает исключение, сообщающее, что запись новых запросов запрещена , и он не нашел существующую запись.

Вопрос

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 Code Spelunking

Внутренние компоненты для запуска одного теста с триггерами 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 .

ЦСИ / _pytest / reports.py: L361

longrepr = item.repr_failure(excinfo)

ОБНОВЛЕНИЕ # 1 2019-06-26 : Благодаря некоторым указателям от @ hoefling в комментариях я обновил свой conftest.py .

  • Правильно повторно вызвать исключение, используя форму raise ... from ....
  • Переопределите _vcr_marker, чтобы прикрепить приборы vcr и vcr_cassette к request.node, которые представляют этот отдельный тестовый элемент.
  • Остальное : Получить доступ к перехваченному запросу из пропатченного VCRConnection ...

ОБНОВЛЕНИЕ № 2 2019-06-26

Казалось бы, невозможно получить доступ к VCRHTTPConnections, которые были исправлены при создании диспетчера контекста кассеты. Я открыл следующий запрос извлечения, чтобы передать в качестве аргументов при возникновении исключения, а затем перехватить и обработать произвольно вниз по течению.

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

Относящиеся

Связанные вопросы, которые являются информативными, но все еще не отвечают на этот вопрос.

1 Ответ

0 голосов
/ 01 июля 2019

Спасибо за комментарии и указания в комментариях от @ 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) объединенный запрос на извлечение еще не передан в PyPI, поэтому при использовании этой опции установки pip по ссылке github будет выпущена предварительная версия:

pip install из ветки git repo

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

Переопределить pytest_exception_interact крюк

В корне вашей тестовой директории создайте conftest.py to , создайте локальный плагин , который переопределяет 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

Извлеките Cassette и Request из исключения.

# 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)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...