0

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?

halfer
  • 19,824
  • 17
  • 99
  • 186
writes_on
  • 1,735
  • 2
  • 22
  • 35
  • It might help to give the `-v` flag to pytest to run in verbose mode (it might give you more information about the error). I'd also recommend using `pip install -r requirements.txt` in your Github actions file to avoid having to make sure that you have the same dependencies in both locations and update multiple files. – MatsLindh Oct 07 '22 at 13:19
  • Thanks for the suggestions; both are good! I'll try them now. – writes_on Oct 07 '22 at 16:49

0 Answers0