Testing API#

fastapi_restly.testing provides configure_tests(), which adds schema and isolation behavior to an application already configured for its test database, and the synchronous and asynchronous status-asserting test clients, RestlyTestClient and AsyncRestlyTestClient.

Install the optional testing dependencies before importing this module:

pip install "fastapi-restly[testing]"

The testing extra installs a pytest plugin entry point, so pytest auto-loads the fixtures. If your project disables plugin autoloading, add the following line to your conftest.py:

pytest_plugins = ["fastapi_restly.pytest_fixtures"]

This imports the namespaced fixtures (restly_app, restly_client, restly_async_client, restly_session, restly_async_session, etc.) into your test session without needing to import them individually.

class fastapi_restly.testing.AsyncRestlyTestClient(app: Any, *args: Any, _transport_app: Any | None = None, **kwargs: Any)#

Bases: _StatusAssertions, AsyncClient

Async test client with the same response assertions as the sync client.

async delete(url: URL | str, *, assert_status_code: int | None = 204, **kwargs: Any) Response#

Make a DELETE request and assert status 204 by default.

Pass assert_status_code=None to accept any status below 400; it does not skip the check.

async get(url: URL | str, *, assert_status_code: int | None = 200, **kwargs: Any) Response#

Make a GET request and assert status 200 by default.

Pass assert_status_code=None to accept any status below 400; it does not skip the check.

async patch(url: URL | str, *, assert_status_code: int | None = 200, **kwargs: Any) Response#

Make a PATCH request and assert status 200 by default.

Pass assert_status_code=None to accept any status below 400; it does not skip the check.

async post(url: URL | str, *, assert_status_code: int | None = 201, **kwargs: Any) Response#

Make a POST request and assert status 201 by default.

Pass assert_status_code=None to accept any status below 400; it does not skip the check.

async put(url: URL | str, *, assert_status_code: int | None = 200, **kwargs: Any) Response#

Make a PUT request and assert status 200 by default.

Pass assert_status_code=None to accept any status below 400; it does not skip the check.

class fastapi_restly.testing.RestlyTestClient(app: Any, *args: Any, _transport_app: Any | None = None, **kwargs: Any)#

Bases: _StatusAssertions, TestClient

Synchronous test client with Restly’s response status assertions.

delete(url: URL | str, *, assert_status_code: int | None = 204, **kwargs: Any) Response#

Make a DELETE request. Asserts the response status code matches assert_status_code (default: 204). Pass assert_status_code=None to accept any status below 400 instead; it does not skip the check.

get(url: URL | str, *, assert_status_code: int | None = 200, **kwargs: Any) Response#

Make a GET request. Asserts the response status code matches assert_status_code (default: 200). Pass assert_status_code=None to accept any status below 400 instead; it does not skip the check.

patch(url: URL | str, *, assert_status_code: int | None = 200, **kwargs: Any) Response#

Make a PATCH request. Asserts the response status code matches assert_status_code (default: 200). Pass assert_status_code=None to accept any status below 400 instead; it does not skip the check.

post(url: URL | str, *, assert_status_code: int | None = 201, **kwargs: Any) Response#

Make a POST request. Asserts the response status code matches assert_status_code (default: 201). Pass assert_status_code=None to accept any status below 400 instead; it does not skip the check.

put(url: URL | str, *, assert_status_code: int | None = 200, **kwargs: Any) Response#

Make a PUT request. Asserts the response status code matches assert_status_code (default: 200). Pass assert_status_code=None to accept any status below 400 instead; it does not skip the check.

fastapi_restly.testing.configure_tests(*, app: FastAPI | None = None, base: type[DeclarativeBase] | MetaData | None = None, create_all: bool = False, alembic_upgrade: bool | str | Path = False, db_cleanup: str = 'rollback', db_cleanup_exclude: Sequence[str] = ()) None#

Add Restly’s managed testing behaviour to an already-configured app.

Call it from conftest.py after the application has configured Restly for its test database:

import fastapi_restly as fr
from myapp.main import app
from myapp.models import Base

fr.testing.configure_tests(
    app=app,
    base=Base,
    create_all=True,
)

configure_tests() never chooses, creates or replaces a database engine. The application owns that configuration through fastapi_restly.configure(); this function records the session factories already in force. Point the application at a disposable test database before importing it, or construct it from explicit test settings. Database configuration performed afterwards is rejected so schema setup, cleanup and requests cannot disagree.

app becomes what the restly_app fixture returns. The schema is optionally built once before tests start, and every test gets a clean database by the strategy db_cleanup names. Client-only tests are covered too.

base= names the models the suite works with. It is what delete mode empties between tests, and what create_all builds. Pass your declarative base, or its MetaData.

Who builds the schema is a separate choice, and the two are mutually exclusive:

  • create_all=True builds it straight from base, as fastapi_restly.db.create_all() does.

  • alembic_upgrade=True runs alembic upgrade head through alembic.ini next to your project root; pass a path to point at a different config. Restly reads the URL from the application’s configured engine and sets sqlalchemy.url on the config. It preserves that URL’s driver, so an async-only application needs an Alembic env.py created from the async template, or otherwise adapted to run migrations with an async DBAPI.

  • Passing neither leaves the schema to you, which is the right choice when your suite already builds it or your migrations are not Alembic.

The application is responsible for selecting a database that tests may modify. In particular, create_all=True and db_cleanup="delete" must only be used against a disposable test database. One call configures the whole pytest process; a second call raises.

db_cleanup chooses how each test gets a clean database:

  • "rollback" (the default) wraps every test in a transaction that is rolled back through a savepoint when the test ends. Nothing is ever committed, which makes it the fastest option and the reason no other process can see a test’s data, not even after a failure.

  • "delete" empties the tables before each test instead, and lets writes commit for real. It is slower, but the last test’s rows are still in the database when the run ends, so you can inspect them with ordinary tools; run with -k and the last test is the one you are looking at. Tests still cannot see each other’s data, and it needs a database of its own: two suites deleting from one database will fight. A normally pooled async engine is supported: Restly disposes its checked-in connections at the event-loop boundaries the test fixtures create.

  • "none" cleans nothing and leaves it to you. Reach for it when neither of the others fits: tests that drive a browser or a second process (nothing uncommitted is visible to those), or parallel workers sharing one database, whose cleaning would collide.

RESTLY_DB_CLEANUP overrides this argument, and --restly-db-cleanup overrides both, so a debugging run can switch mode without editing the suite.

db_cleanup_exclude names tables cleaning must leave alone. Reference data seeded by a migration is the usual reason: cleaning would empty those tables before the first test and nothing would put the rows back. A table under a non-default schema is named with its qualifier, "tenant.item". Naming a table that does not exist raises, so a typo cannot silently drop the protection. Excluded tables are shared by every test, so writes to them do leak between tests.

See also

Test APIs with RestlyTestClient and Fixtures covers the setup, the cleanup modes, the fixture reference, and how the rollback works.