Разобраться с влиянием debug-режима на тестирование в Docker'е
Поведение тестов при запуске pytest в Docker-контейнере меняется в зависимости от того, какое значение имеет параметр DEBUG в .env-файле. При включенном debug-режиме (DEBUG=True в .env-файле) ломается часть тестов, в которых тестовый клиент должен дать на http-запрос ответ со статусом 200. Вместо этого тестовый клиент дает ответ со статусом 429, как будто условному боту, от которого поступают запросы, был выдан бан. При этом по-видимому статус 429 в ответе тестового клиента не связан с количеством запросов, так как всего два теста, в каждом из которых выполняется по одному запросу, приводят к статусу 429 в ответе.
Если debug-режим выключен (DEBUG=False в .env-файле), ломается тест, в котором ожидается напротив, получение бана, то есть ожидается статус-код 429 в ответе от тестового клиента.
Пример результата теста при DEBUG=True в .env-файле:
docker compose run --rm tg_proxy pytest
============================================================================================================ test session starts =============================================================================================================
platform linux -- Python 3.12.1, pytest-7.4.3, pluggy-1.4.0
rootdir: /usr/src/app
plugins: anyio-4.3.0, httpx-0.30.0
collected 6 items
test_fastapi_proxy.py ...FE.E. [100%]
=================================================================================================================== ERRORS ===================================================================================================================
___________________________________________________________________________________________________ ERROR at teardown of test_send_message ___________________________________________________________________________________________________
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x7fa3c1bf7620>, assert_all_responses_were_requested = True, non_mocked_hosts = []
@pytest.fixture
def httpx_mock(
monkeypatch: MonkeyPatch,
assert_all_responses_were_requested: bool,
non_mocked_hosts: List[str],
) -> Generator[HTTPXMock, None, None]:
# Ensure redirections to www hosts are handled transparently.
missing_www = [
f"www.{host}" for host in non_mocked_hosts if not host.startswith("www.")
]
non_mocked_hosts += missing_www
mock = HTTPXMock()
# Mock synchronous requests
real_handle_request = httpx.HTTPTransport.handle_request
def mocked_handle_request(
transport: httpx.HTTPTransport, request: httpx.Request
) -> httpx.Response:
if request.url.host in non_mocked_hosts:
return real_handle_request(transport, request)
return mock._handle_request(transport, request)
monkeypatch.setattr(
httpx.HTTPTransport,
"handle_request",
mocked_handle_request,
)
# Mock asynchronous requests
real_handle_async_request = httpx.AsyncHTTPTransport.handle_async_request
async def mocked_handle_async_request(
transport: httpx.AsyncHTTPTransport, request: httpx.Request
) -> httpx.Response:
if request.url.host in non_mocked_hosts:
return await real_handle_async_request(transport, request)
return await mock._handle_async_request(transport, request)
monkeypatch.setattr(
httpx.AsyncHTTPTransport,
"handle_async_request",
mocked_handle_async_request,
)
yield mock
> mock.reset(assert_all_responses_were_requested)
/usr/local/lib/python3.12/site-packages/pytest_httpx/__init__.py:76:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <pytest_httpx._httpx_mock.HTTPXMock object at 0x7fa3c1bf7920>, assert_all_responses_were_requested = True
def reset(self, assert_all_responses_were_requested: bool) -> None:
self._requests.clear()
not_called = self._reset_callbacks()
if assert_all_responses_were_requested:
matchers_description = "\n".join([str(matcher) for matcher in not_called])
> assert (
not not_called
), f"The following responses are mocked but not requested:\n{matchers_description}"
E AssertionError: The following responses are mocked but not requested:
E Match all requests on https://api.telegram.org/bot6391360892:AAG23mjz8FvyZkTj7Xj-YSoZbezzt16NK8U/sendMessage
E assert not [<pytest_httpx._request_matcher._RequestMatcher object at 0x7fa3c28406e0>]
/usr/local/lib/python3.12/site-packages/pytest_httpx/_httpx_mock.py:257: AssertionError
____________________________________________________________________________________________________ ERROR at teardown of test_send_queue ____________________________________________________________________________________________________
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x7fa3c132f0b0>, assert_all_responses_were_requested = True, non_mocked_hosts = []
@pytest.fixture
def httpx_mock(
monkeypatch: MonkeyPatch,
assert_all_responses_were_requested: bool,
non_mocked_hosts: List[str],
) -> Generator[HTTPXMock, None, None]:
# Ensure redirections to www hosts are handled transparently.
missing_www = [
f"www.{host}" for host in non_mocked_hosts if not host.startswith("www.")
]
non_mocked_hosts += missing_www
mock = HTTPXMock()
# Mock synchronous requests
real_handle_request = httpx.HTTPTransport.handle_request
def mocked_handle_request(
transport: httpx.HTTPTransport, request: httpx.Request
) -> httpx.Response:
if request.url.host in non_mocked_hosts:
return real_handle_request(transport, request)
return mock._handle_request(transport, request)
monkeypatch.setattr(
httpx.HTTPTransport,
"handle_request",
mocked_handle_request,
)
# Mock asynchronous requests
real_handle_async_request = httpx.AsyncHTTPTransport.handle_async_request
async def mocked_handle_async_request(
transport: httpx.AsyncHTTPTransport, request: httpx.Request
) -> httpx.Response:
if request.url.host in non_mocked_hosts:
return await real_handle_async_request(transport, request)
return await mock._handle_async_request(transport, request)
monkeypatch.setattr(
httpx.AsyncHTTPTransport,
"handle_async_request",
mocked_handle_async_request,
)
yield mock
> mock.reset(assert_all_responses_were_requested)
/usr/local/lib/python3.12/site-packages/pytest_httpx/__init__.py:76:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <pytest_httpx._httpx_mock.HTTPXMock object at 0x7fa3c132eb10>, assert_all_responses_were_requested = True
def reset(self, assert_all_responses_were_requested: bool) -> None:
self._requests.clear()
not_called = self._reset_callbacks()
if assert_all_responses_were_requested:
matchers_description = "\n".join([str(matcher) for matcher in not_called])
> assert (
not not_called
), f"The following responses are mocked but not requested:\n{matchers_description}"
E AssertionError: The following responses are mocked but not requested:
E Match all requests on https://api.telegram.org/bot6391360892:AAG23mjz8FvyZkTj7Xj-YSoZbezzt16NK8U/sendMessage
E assert not [<pytest_httpx._request_matcher._RequestMatcher object at 0x7fa3c132d070>]
/usr/local/lib/python3.12/site-packages/pytest_httpx/_httpx_mock.py:257: AssertionError
================================================================================================================== FAILURES ==================================================================================================================
_____________________________________________________________________________________________________________ test_send_message ______________________________________________________________________________________________________________
httpx_mock = <pytest_httpx._httpx_mock.HTTPXMock object at 0x7fa3c1bf7920>
def test_send_message(httpx_mock):
url = f"/bot{settings.tg_token}/sendMessage"
httpx_mock.add_response(url=f'https://api.telegram.org{url}')
content = {"chat_id": 1365913221, "text": "test for test_send_message"}
with TestClient(app) as client:
response = client.post(url, json=content)
> assert response.status_code == 200
E assert 429 == 200
E + where 429 = <Response [429 Too Many Requests]>.status_code
test_fastapi_proxy.py:58: AssertionError
============================================================================================================== warnings summary ==============================================================================================================
test_fastapi_proxy.py::test_send_document
test_fastapi_proxy.py::test_send_message_with_buttons
test_fastapi_proxy.py::test_status
test_fastapi_proxy.py::test_send_message
test_fastapi_proxy.py::test_send_queue
test_fastapi_proxy.py::test_send_queue_without_mock
/usr/local/lib/python3.12/site-packages/httpx/_client.py:680: DeprecationWarning: The 'app' shortcut is now deprecated. Use the explicit style 'transport=WSGITransport(app=...)' instead.
warnings.warn(message, DeprecationWarning)
test_fastapi_proxy.py::test_send_document
test_fastapi_proxy.py::test_send_message_with_buttons
/usr/local/lib/python3.12/site-packages/httpx/_content.py:202: DeprecationWarning: Use 'content=<...>' to upload raw bytes/text content.
warnings.warn(message, DeprecationWarning)
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
========================================================================================================== short test summary info ===========================================================================================================
FAILED test_fastapi_proxy.py::test_send_message - assert 429 == 200
ERROR test_fastapi_proxy.py::test_send_message - AssertionError: The following responses are mocked but not requested:
ERROR test_fastapi_proxy.py::test_send_queue - AssertionError: The following responses are mocked but not requested:
============================================================================================= 1 failed, 5 passed, 8 warnings, 2 errors in 0.37s ==============================================================================================
Пример результата теста при DEBUG=False в .env-файле:
============================================================================================================ test session starts =============================================================================================================
platform linux -- Python 3.12.2, pytest-8.1.1, pluggy-1.4.0
rootdir: /home/sergey/PyProjects/internship/tg-flood-limiter
plugins: anyio-4.3.0, httpx-0.30.0
collected 6 items
app/test_fastapi_proxy.py ....F. [100%]
================================================================================================================== FAILURES ==================================================================================================================
______________________________________________________________________________________________________________ test_send_queue _______________________________________________________________________________________________________________
httpx_mock = <pytest_httpx._httpx_mock.HTTPXMock object at 0x7f4501271190>
def test_send_queue(httpx_mock):
url = f"/bot{settings.tg_token}/sendMessage"
httpx_mock.add_response(url=f'https://api.telegram.org{url}')
have_ban = False
with TestClient(app) as client:
for _ in range(1, 12):
content = {"chat_id": "1365913221", "text": f"test time {time.time()}"}
response = client.post(url, json=content)
if response.status_code == 429:
have_ban = True
break
content = {"chat_id": "2125368673", "text": f"test time {time.time()}"}
response = client.post(url, json=content)
if response.status_code == 429:
have_ban = True
break
> assert have_ban == True
E assert False == True
app/test_fastapi_proxy.py:79: AssertionError
============================================================================================================== warnings summary ==============================================================================================================
app/test_fastapi_proxy.py::test_send_document
app/test_fastapi_proxy.py::test_send_message_with_buttons
app/test_fastapi_proxy.py::test_status
app/test_fastapi_proxy.py::test_send_message
app/test_fastapi_proxy.py::test_send_queue
app/test_fastapi_proxy.py::test_send_queue_without_mock
/home/sergey/PyProjects/internship/tg-flood-limiter/venv/lib64/python3.12/site-packages/httpx/_client.py:680: DeprecationWarning: The 'app' shortcut is now deprecated. Use the explicit style 'transport=WSGITransport(app=...)' instead.
warnings.warn(message, DeprecationWarning)
app/test_fastapi_proxy.py: 29 warnings
/home/sergey/PyProjects/internship/tg-flood-limiter/venv/lib64/python3.12/site-packages/httpx/_content.py:202: DeprecationWarning: Use 'content=<...>' to upload raw bytes/text content.
warnings.warn(message, DeprecationWarning)
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
========================================================================================================== short test summary info ===========================================================================================================
FAILED app/test_fastapi_proxy.py::test_send_queue - assert False == True
================================================================================================= 1 failed, 5 passed, 35 warnings in 17.35s ==================================================================================================