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

Commit

Permalink
feat(testing): pytest plugin (#246)
Browse files Browse the repository at this point in the history
Adds an initial pass at a pytest plugin for testing `starlite-saqlalchemy` applications.

Includes the following fixtures:
* `is_unit_test`: a boolean that distinguishes between unit and integration tests
* `_patch_http_close`: an autouse fixture that prevents any globally instantiated http clients being closed between tests
* `_patch_sqlalchemy_plugin`: autouse fixture that patches the sqlalchemy plugin `on_shutdown` method for unittests
* `_patch_worker`: autouse fixture that patches the worker `on_app_startup` and `stop` methods for unit tests
* `app`: fixture that uses a defined import path to inject an application instance. Default path is `app.main:create_app`
* `client`: fixture that injects a starlite `TestClient` instance bound to the same app given by the `app` fixture
* `cap_logger`: a structlog capturing logger for inspecting log output

Closes #106
Closes #232
  • Loading branch information
peterschutt committed Jan 16, 2023
1 parent 740f3f7 commit 6b09323
Show file tree
Hide file tree
Showing 28 changed files with 1,367 additions and 999 deletions.
3 changes: 2 additions & 1 deletion .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ type-checking-exempt-modules = from sqlalchemy.orm
per-file-ignores =
examples/dto/*:T201,TC
examples/tests/*:SCS108,PT013
src/pytest_starlite_saqlalchemy/__init__.py:F401
src/starlite_saqlalchemy/dependencies.py:TC
src/starlite_saqlalchemy/health.py:TC
src/starlite_saqlalchemy/repository/filters.py:TC
src/starlite_saqlalchemy/scripts.py:T201
src/starlite_saqlalchemy/settings.py:TC
src/starlite_saqlalchemy/testing.py:SCS108
src/starlite_saqlalchemy/testing/controller_test.py:SCS108
src/starlite_saqlalchemy/users/controllers.py:TC
tests/*:SCS108,PT013
tests/integration/test_tests.py:TC002,SCS108
Expand Down
11 changes: 5 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ on:
push:
branches: [main]
pull_request:
branches: [main]

env:
FORCE_COLOR: "1"
Expand Down Expand Up @@ -35,7 +34,7 @@ jobs:
with:
python-version: ${{env.PYTHON_LATEST}}
- name: Install Dependencies
run: python -m pip install --upgrade wheel tox
run: python -m pip install --upgrade wheel tox==4.2.8
- run: python -m tox -e pylint
mypy:
name: mypy
Expand All @@ -56,7 +55,7 @@ jobs:
with:
python-version: ${{env.PYTHON_LATEST}}
- name: Install Dependencies
run: python -m pip install --upgrade wheel tox
run: python -m pip install --upgrade wheel tox==4.2.8
- run: python -m tox -e mypy
pyright:
name: pyright
Expand All @@ -78,7 +77,7 @@ jobs:
with:
python-version: ${{env.PYTHON_LATEST}}
- name: Install Dependencies
run: python -m pip install --upgrade wheel tox
run: python -m pip install --upgrade wheel tox==4.2.8
- run: python -m tox -e pyright
tests:
name: tests on ${{matrix.python-version}}
Expand Down Expand Up @@ -108,7 +107,7 @@ jobs:
with:
python-version: ${{matrix.python-version}}
- name: Install Dependencies
run: python -m pip install --upgrade wheel tox tox-gh-actions
run: python -m pip install --upgrade wheel tox==4.2.8 tox-gh-actions
- run: python -m tox
- name: Upload Coverage Data
uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # @v3.1.1
Expand All @@ -133,7 +132,7 @@ jobs:
- uses: actions/setup-python@5ccb29d8773c3f3f653e1705f474dfaa8a06a912 #v4.4.0
with:
python-version: ${{env.PYTHON_LATEST}}
- run: python -m pip install --upgrade wheel tox
- run: python -m pip install --upgrade wheel tox==4.2.8
- name: Download coverage data
uses: actions/download-artifact@9782bd6a9848b53b110e712e20e42d89988822b7 # @v3.0.1
with:
Expand Down
137 changes: 137 additions & 0 deletions docs/testing/pytest_plugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# Pytest Plugin

The nature of applications built with the `starlite-saqlalchemy` pattern is that they rely heavily
on connected services.

Abstraction of [PostgreSQL][2] and [Redis][3] connectivity boilerplate is a nice convenience,
however to successfully patch the application for testing requires deeper knowledge of the
implementation than would be otherwise necessary.

So, `starlite-saqlalchemy` ships with a selection of [pytest fixtures][1] that are often necessary
when building applications such as these.

## `app`

The `app` fixture provides an instance of a `Starlite` application.

```python
from __future__ import annotations

from starlite import Starlite


def test_app_fixture(app: Starlite) -> None:
assert isinstance(app, Starlite)
```

The value of Pytest ini option, `test_app` is used to determine the application to load.

```toml
# pyproject.toml

[tool.pytest.ini_options]
test_app = "app.main:create_app"
```

If no value is configured for the `test_app` ini option, the default location of
`"app.main:create_app"` is searched.

The value of the `test_app` ini option can either point to an application factory or `Starlite`
instance.

If the object found at the import path is not a `Starlite` instance, the fixture assumes it is
an application factory, and will call the object and return the response.

The value of `test_app` is resolved using the uvicorn `import_from_string()` function, so it
supports the same format as `uvicorn` supports for its `app` and `factory` parameters.

## `client`

A `starlite.testing.TestClient` instance, wired to the same application that is produced by the
`app` fixture.

## `cap_logger`

The `cap_logger` fixture provides an instance of [`structlog.testing.CapturingLogger`][4].

```python
from __future__ import annotations

from typing import TYPE_CHECKING

from structlog.testing import CapturedCall

if TYPE_CHECKING:
from structlog.testing import CapturingLogger


def test_app_fixture(cap_logger: CapturingLogger) -> None:
cap_logger.info("hello")
cap_logger.info("hello", when="again")
assert cap_logger.calls == [
CapturedCall(method_name="info", args=("hello",), kwargs={}),
CapturedCall(method_name="info", args=("hello",), kwargs={"when": "again"}),
]
```

The `cap_logger` fixture will capture any `structlog` calls made by the starlite application or the
SAQ worker, so that they can be inspected as part of tests.

```python
from __future__ import annotations

from typing import TYPE_CHECKING

from httpx import AsyncClient

if TYPE_CHECKING:
from starlite import Starlite
from structlog.testing import CapturingLogger


async def test_health_logging_skipped(
app: Starlite, cap_logger: CapturingLogger
) -> None:
"""Test that calls to the health check route are not logged."""

async with AsyncClient(app=app, base_url="http://testserver") as client:
response = await client.get("/health")
assert response.status_code == 200

assert [] == cap_logger.calls
```

## is_unit_test

The `is_unit_test` fixture returns a `bool` that indicates if the test suite believes it is running
a unit test, or an integration test.

To determine this, we compare the path of the running test to the value of the Pytest ini option
`unit_test_pattern`, which by default is `"^.*/tests/unit/.*$"`.

This fixture is used to make fixtures behave differently between unit and integration test contexts.

## _patch_http_close

This is an [`autouse` fixture][5], that prevents HTTP clients that are defined in the global scope
from being closed.

The application is configured to close all instantiated HTTP clients on app shutdown, however when
apps are defined in a global/class scope, a test that runs after the first application shutdown in
the test suite would fail.

## _patch_sqlalchemy_plugin

This is an [`autouse` fixture][5], that mocks out the `on_shutdown` method of the SQLAlchemy config
object for unit tests.

## _patch_worker

This is an [`autouse` fixture][5], that mocks out the `on_app_startup` and `stop` methods of
`worker.Worker` type for unit tests.

[1]: https://docs.pytest.org/en/latest/explanation/fixtures.html#about-fixtures
[2]: https://www.postgresql.org/
[3]: https://redis.io
[4]: https://www.structlog.org/en/stable/api.html#structlog.testing.CapturingLogger
[5]: https://docs.pytest.org/en/6.2.x/fixture.html#autouse-fixtures-fixtures-you-don-t-have-to-request
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ nav:
- DTOs: dto.md
- Async Worker: async_worker.md
- Logging: logging.md
- Testing:
- Pytest Plugin: testing/pytest_plugin.md
- Reference: reference/
watch:
- src/starlite_saqlalchemy
Expand Down
Loading

0 comments on commit 6b09323

Please sign in to comment.