Skip to content

Commit

Permalink
mock AsyncClient with app for performance gains
Browse files Browse the repository at this point in the history
  • Loading branch information
nachomaiz committed Oct 27, 2023
1 parent 0784491 commit b089766
Show file tree
Hide file tree
Showing 9 changed files with 91 additions and 60 deletions.
2 changes: 2 additions & 0 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ Another change is that the order of parameters has been altered. The main reason
#### Internal

- :bug: Fixed typing of `tuple` internal `docs_deploy` function annotations not being compatible with Python <3.9.
- :rocket: Removed slow and unnecessary test for the `verbose` parameter in `HTTPClient`.
- :rocket: Refactored tests that instantiate `httpx.AsyncClient` instances for a 20x reduction in test run time.

#### Docs

Expand Down
4 changes: 0 additions & 4 deletions docs/usage/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,6 @@ bavapi.Query(
)
```

1. !!! tip "Stick with defaults"

The default `per_page` value (`100`) has been set after testing various options for the best download speed. :rocket:

### `Query` parameters

All Fount queries performed with [`bavapi.Query`][query.Query] support the following parameters:
Expand Down
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -93,5 +93,4 @@ mypy_path = "type_stubs"
[tool.pytest.ini_options]
markers = [
"e2e: tests that require an authenticated connection with the Fount.",
"slow: slow integration tests that don't require a Fount connection."
]
22 changes: 20 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,40 @@
# pylint: disable=redefined-outer-name, missing-function-docstring, missing-module-docstring
# pylint: disable=protected-access

from typing import Iterator
from unittest import mock

import httpx
import pytest

from bavapi.client import Client
from bavapi.http import HTTPClient

from tests.helpers import mock_app


@pytest.fixture(scope="session")
def anyio_backend() -> str:
return "asyncio"


@pytest.fixture(scope="session")
def client() -> HTTPClient:
return HTTPClient("test")
def http_client():
return httpx.AsyncClient(app=mock_app)


@pytest.fixture(scope="session")
def client(http_client: httpx.AsyncClient) -> HTTPClient:
return HTTPClient(client=http_client)


@pytest.fixture(scope="session")
def fount(client: HTTPClient) -> Client:
return Client(client=client)


@pytest.fixture
def mock_async_client() -> Iterator[httpx.AsyncClient]:
async_client = httpx.AsyncClient(app=mock_app)
with mock.patch("httpx.AsyncClient", return_value=async_client) as mock_client:
yield mock_client
5 changes: 5 additions & 0 deletions tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import Callable, Optional, Type, TypeVar, Union

T = TypeVar("T")
ASGIApp = Callable[..., "ASGIApp"]


def wraps(
Expand All @@ -16,3 +17,7 @@ def _wraps(*_, **__):
return return_value

return _wraps


def mock_app(*args, **kwargs) -> ASGIApp:
return mock_app(*args, **kwargs) # pragma: no cover
14 changes: 12 additions & 2 deletions tests/reference/test_generate_reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ def test_parse_args_all():


@mock.patch("bavapi._reference.generate_reference.os.getenv", return_value="test_token")
def test_main_no_args(mock_getenv: mock.Mock):
def test_main_no_args(mock_getenv: mock.Mock, mock_async_client: mock.MagicMock):
with pytest.raises(ValueError) as exc_info:
uref.main([])

Expand All @@ -131,14 +131,19 @@ def test_main_no_args(mock_getenv: mock.Mock):
"Run `bavapi-gen-refs -h for more details and instructions.",
)
mock_getenv.assert_called_once_with("BAV_API_KEY", "")
mock_async_client.assert_called_once()


@mock.patch(
"bavapi._reference.generate_reference.Client.raw_query",
wraps=wraps([{"id": 1, "name": "A"}, {"id": 2, "name": "B"}]),
)
@mock.patch("bavapi._reference.generate_reference.write_to_file")
def test_main(mock_write_to_file: mock.Mock, mock_raw_query: mock.AsyncMock):
def test_main(
mock_write_to_file: mock.Mock,
mock_raw_query: mock.AsyncMock,
mock_async_client: mock.MagicMock,
):
args = ["-n", "audiences"]

with mock.patch(
Expand All @@ -149,6 +154,7 @@ def test_main(mock_write_to_file: mock.Mock, mock_raw_query: mock.AsyncMock):
assert len(mock_write_to_file.call_args_list) == 2
mock_raw_query.assert_awaited_once_with("audiences", Query())
mock_getenv.assert_called_once_with("BAV_API_KEY", "")
mock_async_client.assert_called_once()


@mock.patch(
Expand All @@ -168,6 +174,7 @@ def test_main_all(
mock_gen_init_source: mock.Mock,
mock_write_to_file: mock.Mock,
mock_raw_query: mock.AsyncMock,
mock_async_client: mock.MagicMock,
):
args = ["-a"]

Expand All @@ -180,6 +187,7 @@ def test_main_all(
assert len(mock_write_to_file.call_args_list) == 3
assert len(mock_raw_query.call_args_list) == 2
mock_getenv.assert_called_once_with("BAV_API_KEY", "")
mock_async_client.assert_called_once()


@mock.patch("dotenv.load_dotenv", wraps=wraps(raises=ImportError))
Expand Down Expand Up @@ -210,6 +218,7 @@ def test_main_with_token_arg(
mock_write_to_file: mock.Mock,
mock_load_dotenv: mock.Mock,
mock_raw_query: mock.AsyncMock,
mock_async_client: mock.MagicMock,
):
args = ["-n", "audiences", "-t", "test_token"]

Expand All @@ -223,3 +232,4 @@ def test_main_with_token_arg(
mock_load_dotenv.assert_not_called()
mock_getenv.assert_called_once_with("BAV_API_KEY", "test_token")
mock_raw_query.assert_awaited_once_with("audiences", Query())
mock_async_client.assert_called_once()
62 changes: 39 additions & 23 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import pytest

from bavapi import filters
from bavapi.client import Client, _default_include
from bavapi.client import BASE_URL, USER_AGENT, Client, _default_include
from bavapi.http import HTTPClient
from bavapi.query import Query
from bavapi.typing import OptionalListOr
Expand All @@ -17,27 +17,41 @@
# CLASS INIT TESTS


def test_init():
def test_init(mock_async_client: mock.MagicMock):
fount = Client("test_token")
mock_async_client.assert_called_once_with(
headers={
"Authorization": "Bearer test_token",
"Accept": "application/json",
"User-Agent": USER_AGENT,
},
timeout=30.0,
verify=True,
base_url=BASE_URL,
)
assert isinstance(fount._client, HTTPClient)


def test_init_with_client(client: HTTPClient):
fount = Client(client=client)
assert fount._client is client


def test_init_no_token():
with pytest.raises(ValueError) as excinfo:
Client()

assert excinfo.value.args[0] == "You must provide `auth_token` or `client`."


def test_init_user_agent():
fount = Client("test_token", user_agent="TEST_AGENT")

assert fount._client.client.headers["User-Agent"] == "TEST_AGENT"
def test_init_user_agent(mock_async_client: mock.MagicMock):
fount = Client("test_token", user_agent="test_agent")
mock_async_client.assert_called_once_with(
headers={
"Authorization": "Bearer test_token",
"Accept": "application/json",
"User-Agent": "test_agent",
},
timeout=30.0,
verify=True,
base_url=BASE_URL,
)
assert isinstance(fount._client, HTTPClient)


# PRIVATE TESTS
Expand Down Expand Up @@ -67,23 +81,25 @@ def test_no_default_include():
# PUBLIC TESTS


def test_per_page():
_fount = Client("token")
assert _fount.per_page == 100
_fount.per_page = 1000
assert _fount._client.per_page == 1000
def test_per_page(fount: Client):
assert fount.per_page == 100
fount.per_page = 1000
assert fount._client.per_page == 1000

fount.per_page = 100


def test_verbose(fount: Client):
assert fount.verbose
fount.verbose = False
assert not fount._client.verbose

def test_verbose():
_fount = Client("token")
assert _fount.verbose
_fount.verbose = False
assert not _fount._client.verbose
fount.verbose = True


@pytest.mark.anyio
async def test_context_manager():
_fount = Client(client=HTTPClient())
async def test_context_manager(mock_async_client: mock.MagicMock):
_fount = Client(client=HTTPClient(client=mock_async_client))
async with _fount as fount_ctx:
assert isinstance(fount_ctx, Client)

Expand Down
31 changes: 7 additions & 24 deletions tests/test_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,29 +81,26 @@ def rate_limited_response() -> httpx.Response:
# TESTS


def test_client_init_with_client():
httpx_client = httpx.AsyncClient()
def test_client_init_with_client(http_client: httpx.AsyncClient):
client = HTTPClient(client=http_client)

client = HTTPClient(client=httpx_client)

assert client.client is httpx_client
assert client.client is http_client


@pytest.mark.anyio
async def test_context_manager():
client = HTTPClient()
async with client as client:
async def test_context_manager(mock_async_client: mock.MagicMock):
async with HTTPClient(client=mock_async_client) as client:
assert isinstance(client, HTTPClient)

assert client.client.is_closed


@pytest.mark.anyio
@mock.patch("bavapi.http.httpx.AsyncClient.aclose", wraps=wraps())
@mock.patch("bavapi.http.httpx.AsyncClient.aclose")
async def test_aclose(mock_aclose: mock.AsyncMock, client: HTTPClient):
await client.aclose()

mock_aclose.assert_called_once()
mock_aclose.assert_awaited_once()


@pytest.mark.anyio
Expand Down Expand Up @@ -169,20 +166,6 @@ async def test_get_pages(
mock_get.assert_awaited_once_with("request", Query(page=1, per_page=100))


@pytest.mark.anyio
@mock.patch("bavapi.http.HTTPClient.get", wraps=wraps(["page"]))
async def test_get_pages_no_pbar(
mock_get: mock.AsyncMock, capsys: pytest.CaptureFixture
):
client = HTTPClient("test", verbose=False)
res = await client.get_pages("request", Query(), 1)
captured = capsys.readouterr()

assert res == [["page"]]
assert not captured.err
mock_get.assert_awaited_once_with("request", Query(page=1, per_page=100))


@pytest.mark.anyio
@mock.patch("bavapi.http.HTTPClient.get", wraps=wraps(raises=ValueError))
async def test_get_pages_fails(mock_get: mock.AsyncMock, client: HTTPClient):
Expand Down
10 changes: 6 additions & 4 deletions tests/test_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,14 @@ async def mock_func():
mock_running_in_jupyter.assert_called_once()


@pytest.mark.slow
def test_raw_query():
def test_raw_query(mock_async_client: mock.MagicMock):
with mock.patch("bavapi.sync.Client.raw_query", wraps=wraps()) as mock_raw_query:
sync.raw_query("TOKEN", "companies", Query(), timeout=10.0)

mock_raw_query.assert_called_with("companies", Query())
mock_async_client.assert_called_once()


@pytest.mark.slow
@pytest.mark.parametrize(
("endpoint", "filters"),
(
Expand All @@ -64,8 +63,11 @@ def test_raw_query():
("studies", {}),
),
)
def test_function(endpoint: str, filters: Dict[str, Any]):
def test_function(
endpoint: str, filters: Dict[str, Any], mock_async_client: mock.MagicMock
):
with mock.patch(f"bavapi.sync.Client.{endpoint}", wraps=wraps()) as mock_endpoint:
getattr(sync, endpoint)("TOKEN", filters=filters, timeout=10.0)

mock_endpoint.assert_called_once()
mock_async_client.assert_called_once()

0 comments on commit b089766

Please sign in to comment.