I'm having problems with my unit tests that test Python FastAPI code that uses the httpx module for async HTTP calls. When I run the application in a Docker container and then run the pytests, they all pass. However, when I push the code to GitHub, I get test failures.
Here's the Dockerfile
FROM python:3.9.7-slim
WORKDIR /app/
ENV PYTHONPATH "${PYTHONPATH}:/"
COPY requirements.txt .
RUN pip install -r requirements.txt
CMD ["uvicorn", "main:app", "--reload", "--host", "0.0.0.0"]
And here's the requirements.txt
file it uses:
fastapi==0.82.0
uvicorn==0.18.3
pydantic==1.10.2
pylint==2.15.2
pytest==7.1.3
boto3==1.24.68
python-dotenv==0.21.0
pytest-mock==3.8.2
sseclient==0.0.27
pytest-asyncio==0.19.0
wheel==0.37.1
httpx==0.23.0
pytest-httpx==0.21.0
pytest-trio==0.7.0
And here's the command I run inside the Docker container to run the tests:
pytest --cov=app /tests/unit/ --asyncio-mode=strict
Here's the .github/workflows/unit-tests.yml
file that runs the test on GitHub:
name: unit-tests
on: [push]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 1
ref: ${{ github.event.inputs.branch_name }}
- name: Set up Python 3.9.x
uses: actions/setup-python@v1
with:
python-version: 3.9.x
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install fastapi==0.82.0
pip install pydantic==1.10.2
pip install pytest==7.1.3
pip install boto3==1.24.68
pip install python-dotenv==0.21.0
pip install pytest-mock==3.8.2
pip install sseclient==0.0.27
pip install pytest-asyncio==0.19.0
pip install httpx==0.23.0
pip install pytest-httpx==0.21.0
pip install pytest-cov==4.0.0
pip install pytest-trio==0.7.0
- name: Running unit tests
run: |
pytest --cov=app tests/unit/ --asyncio-mode=strict
As I mentioned the tests succeed when running in the local running Docker container, but fail when run on GitHub. Here is part of the error list produces by the test run on GitHub:
Run pytest --cov=app tests/unit/ --asyncio-mode=strict
============================= test session starts ==============================
platform linux -- Python 3.9.14, pytest-7.1.3, pluggy-1.0.0
rootdir: /home/runner/work/integration-engine/integration-engine
plugins: httpx-0.21.0, anyio-3.6.1, cov-4.0.0, asyncio-0.19.0, mock-3.8.2, trio-0.7.0
asyncio: mode=strict
collected 69 items
tests/unit/test_main.py . [ 1%]
tests/unit/dependencies/test_validate_requests.py .. [ 4%]
tests/unit/helpers/test_agent_command_helper.py ....... [ 14%]
tests/unit/helpers/test_agent_message_helper.py .... [ 20%]
tests/unit/helpers/test_cognito_helper.py ssFEFE [ 26%]
tests/unit/helpers/test_mercure_helper.py ..... [ 33%]
tests/unit/helpers/test_pmb_helper.py .... [ 39%]
tests/unit/helpers/test_repository_helper.py ....... [ 49%]
tests/unit/repositories/test_agent_message_repository.py ... [ 53%]
tests/unit/repositories/test_agent_repository.py ..... [ 60%]
tests/unit/routers/test_agent_communication.py .............. [ 81%]
tests/unit/routers/test_agent_search.py .... [ 86%]
tests/unit/routers/test_authentication.py .. [ 89%]
tests/unit/routers/test_diagnostics.py .. [ 92%]
tests/unit/routers/test_healthz.py .. [ 95%]
tests/unit/routers/test_registration.py ... [100%]
==================================== ERRORS ====================================
_ ERROR at teardown of test_get_token_with_invalid_credentials_returns_error_message[asyncio] _
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x7f5e027aeca0>
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],
) -> HTTPXMock:
# 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_sync_transport = httpx.Client._transport_for_url
monkeypatch.setattr(
httpx.Client,
"_transport_for_url",
lambda self, url: real_sync_transport(self, url)
if url.host in non_mocked_hosts
else _PytestSyncTransport(mock),
)
# Mock asynchronous requests
real_async_transport = httpx.AsyncClient._transport_for_url
monkeypatch.setattr(
httpx.AsyncClient,
"_transport_for_url",
lambda self, url: real_async_transport(self, url)
if url.host in non_mocked_hosts
else _PytestAsyncTransport(mock),
)
yield mock
> mock.reset(assert_all_responses_were_requested)
/opt/hostedtoolcache/Python/3.9.14/x64/lib/python3.9/site-packages/pytest_httpx/__init__.py:65:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <pytest_httpx._httpx_mock.HTTPXMock object at 0x7f5e027ae400>
assert_all_responses_were_requested = True
def reset(self, assert_all_responses_were_requested: bool) -> None:
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
E assert not [<pytest_httpx._httpx_mock._RequestMatcher object at 0x7f5e026a4ac0>]
/opt/hostedtoolcache/Python/3.9.14/x64/lib/python3.9/site-packages/pytest_httpx/_httpx_mock.py:282: AssertionError
_ ERROR at teardown of test_get_token_with_invalid_credentials_returns_error_message[trio] _
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x7f5e02685b80>
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],
) -> HTTPXMock:
# 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_sync_transport = httpx.Client._transport_for_url
monkeypatch.setattr(
httpx.Client,
"_transport_for_url",
lambda self, url: real_sync_transport(self, url)
if url.host in non_mocked_hosts
else _PytestSyncTransport(mock),
)
# Mock asynchronous requests
real_async_transport = httpx.AsyncClient._transport_for_url
monkeypatch.setattr(
httpx.AsyncClient,
"_transport_for_url",
lambda self, url: real_async_transport(self, url)
if url.host in non_mocked_hosts
else _PytestAsyncTransport(mock),
)
yield mock
> mock.reset(assert_all_responses_were_requested)
/opt/hostedtoolcache/Python/3.9.14/x64/lib/python3.9/site-packages/pytest_httpx/__init__.py:65:
What can I try next?