Skip to content
This repository has been archived by the owner on Sep 12, 2023. It is now read-only.

Commit

Permalink
feat(logging): improve dev logging experience
Browse files Browse the repository at this point in the history
- cap log fixture no longer autouse, allows for rich formatting via
pytest
- create integration test client fixture that uses asgi lifespan

closes #107
closes #120
  • Loading branch information
peterschutt committed Nov 17, 2022
1 parent 1a9e01d commit fc04a99
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 29 deletions.
1 change: 1 addition & 0 deletions dev.requirements.txt
@@ -1,3 +1,4 @@
asgi-lifespan == 2.0.0
coverage[toml] == 6.5.0; python_version < '3.11'
coverage == 6.5.0; python_version >= '3.11'
cryptography == 38.0.3
Expand Down
4 changes: 2 additions & 2 deletions src/starlite_saqlalchemy/log/__init__.py
Expand Up @@ -37,10 +37,10 @@
structlog.processors.TimeStamper(fmt="iso", utc=True),
]

if sys.stderr.isatty(): # pragma: no cover
if sys.stderr.isatty() or "pytest" in sys.modules: # pragma: no cover
LoggerFactory: Any = structlog.WriteLoggerFactory
default_processors.extend([structlog.dev.ConsoleRenderer()])
else:
else: # pragma: no cover
LoggerFactory = structlog.BytesLoggerFactory
default_processors.extend(
[
Expand Down
16 changes: 9 additions & 7 deletions tests/conftest.py
Expand Up @@ -2,7 +2,7 @@
from __future__ import annotations

from datetime import date, datetime
from typing import Any
from typing import TYPE_CHECKING
from uuid import UUID

import pytest
Expand All @@ -13,15 +13,16 @@
import starlite_saqlalchemy
from starlite_saqlalchemy import ConfigureApp, log

if TYPE_CHECKING:
from typing import Any

@pytest.fixture(name="cap_logger")
def fx_capturing_logger() -> CapturingLogger:
"""Used to monkeypatch the app logger, so we can inspect output."""
return CapturingLogger()
from pytest import MonkeyPatch


@pytest.fixture(autouse=True)
def _patch_logger(cap_logger: CapturingLogger, monkeypatch: pytest.MonkeyPatch) -> None:
@pytest.fixture(name="cap_logger")
def fx_capturing_logger(monkeypatch: MonkeyPatch) -> CapturingLogger:
"""Used to monkeypatch the app logger, so we can inspect output."""
cap_logger = CapturingLogger()
starlite_saqlalchemy.log.configure(
starlite_saqlalchemy.log.default_processors # type:ignore[arg-type]
)
Expand All @@ -35,6 +36,7 @@ def _patch_logger(cap_logger: CapturingLogger, monkeypatch: pytest.MonkeyPatch)
logger._processors = log.default_processors[:-1]
monkeypatch.setattr(starlite_saqlalchemy.log.controller, "LOGGER", logger)
monkeypatch.setattr(starlite_saqlalchemy.log.worker, "LOGGER", logger)
return cap_logger


@pytest.fixture()
Expand Down
42 changes: 34 additions & 8 deletions tests/integration/conftest.py
@@ -1,12 +1,16 @@
"""Config for integration tests."""
# pylint: disable=redefined-outer-name
from __future__ import annotations

import asyncio
import timeit
from pathlib import Path
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING

import asyncpg
import pytest
from asgi_lifespan import LifespanManager
from httpx import AsyncClient
from redis.asyncio import Redis
from redis.exceptions import ConnectionError as RedisConnectionError
from sqlalchemy.engine import URL
Expand All @@ -19,6 +23,7 @@

if TYPE_CHECKING:
from collections import abc
from typing import Any

from pytest_docker.plugin import Services # type:ignore[import]
from starlite import Starlite
Expand All @@ -28,7 +33,7 @@


@pytest.fixture(scope="session")
def event_loop() -> "abc.Iterator[asyncio.AbstractEventLoop]":
def event_loop() -> abc.Iterator[asyncio.AbstractEventLoop]:
"""Need the event loop scoped to the session so that we can use it to check
containers are ready in session scoped containers fixture."""
policy = asyncio.get_event_loop_policy()
Expand All @@ -47,7 +52,7 @@ def docker_compose_file() -> Path:


async def wait_until_responsive(
check: "abc.Callable[..., abc.Awaitable]", timeout: float, pause: float, **kwargs: Any
check: abc.Callable[..., abc.Awaitable], timeout: float, pause: float, **kwargs: Any
) -> None:
"""Wait until a service is responsive.
Expand Down Expand Up @@ -108,7 +113,7 @@ async def db_responsive(host: str) -> bool:

@pytest.fixture(scope="session", autouse=True)
async def _containers(
docker_ip: str, docker_services: "Services" # pylint: disable=unused-argument
docker_ip: str, docker_services: Services # pylint: disable=unused-argument
) -> None:
"""Starts containers for required services, fixture waits until they are
responsive before returning.
Expand Down Expand Up @@ -162,7 +167,7 @@ async def engine(docker_ip: str) -> AsyncEngine:
@pytest.fixture(autouse=True)
async def _seed_db(
engine: AsyncEngine, raw_authors: list[dict[str, Any]]
) -> "abc.AsyncIterator[None]":
) -> abc.AsyncIterator[None]:
"""Populate test database with.
Args:
Expand All @@ -181,7 +186,7 @@ async def _seed_db(


@pytest.fixture(autouse=True)
def _patch_db(app: "Starlite", engine: AsyncEngine, monkeypatch: pytest.MonkeyPatch) -> None:
def _patch_db(app: Starlite, engine: AsyncEngine, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setitem(app.state, sqlalchemy_plugin.config.engine_app_state_key, engine)
monkeypatch.setitem(
app.state,
Expand All @@ -191,7 +196,7 @@ def _patch_db(app: "Starlite", engine: AsyncEngine, monkeypatch: pytest.MonkeyPa


@pytest.fixture(autouse=True)
def _patch_redis(app: "Starlite", redis: Redis, monkeypatch: pytest.MonkeyPatch) -> None:
def _patch_redis(app: Starlite, redis: Redis, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(app.cache, "backend", redis)
monkeypatch.setattr(worker.queue, "redis", redis)

Expand All @@ -217,7 +222,7 @@ def router() -> Router:


@pytest.fixture()
def app(app: "Starlite", router: Router) -> "Starlite":
def app(app: Starlite, router: Router) -> Starlite:
"""
Args:
app: App from outermost conftest.py
Expand All @@ -228,3 +233,24 @@ def app(app: "Starlite", router: Router) -> "Starlite":
"""
app.register(router)
return app


@pytest.fixture(name="client")
async def fx_client(app: Starlite) -> abc.AsyncIterator[AsyncClient]:
"""Async client that calls requests on the app.
We need to use `httpx.AsyncClient` here, as `starlite.TestClient` creates its own event loop to
run async calls to the underlying app in a sync context, resulting in errors like:
```text
ValueError: The future belongs to a different loop than the one specified as the loop argument
```
Related: https://www.starlette.io/testclient/#asynchronous-tests
The httpx async client will call the app, but not trigger lifecycle events. However, we need
the lifecycle events to be called to configure the logging, hence `LifespanManager`.
"""
async with LifespanManager(app): # type:ignore[arg-type]
async with AsyncClient(app=app, base_url="http://testserver") as client:
yield client
22 changes: 10 additions & 12 deletions tests/integration/test_authors.py
@@ -1,19 +1,17 @@
"""Integration tests for the test Author domain."""
from typing import TYPE_CHECKING
from __future__ import annotations

from httpx import AsyncClient
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from starlite import Starlite
from httpx import AsyncClient


async def test_update_author(app: "Starlite") -> None:
async def test_update_author(client: AsyncClient) -> None:
"""Integration test for PUT route."""

async with AsyncClient(app=app, base_url="http://testserver") as client:
response = await client.put(
"/authors/97108ac1-ffcb-411d-8b1e-d9183399f63b",
json={"name": "TEST UPDATE", "dob": "1890-9-15"},
)
assert response.status_code == 200
assert response.json()["name"] == "TEST UPDATE"
response = await client.put(
"/authors/97108ac1-ffcb-411d-8b1e-d9183399f63b",
json={"name": "TEST UPDATE", "dob": "1890-9-15"},
)
assert response.status_code == 200
assert response.json()["name"] == "TEST UPDATE"

0 comments on commit fc04a99

Please sign in to comment.