Skip to content

Testing with kwik.testing

Kwik ships testing utilities to speed up end‑to‑end and CRUD testing against a real PostgreSQL database. The kwik.testing module includes:

  • Scenario: a fluent builder for users, roles, and permissions
  • IdentityAwareTestClient: a TestClient wrapper that authenticates as specific users
  • Pytest fixtures: ready‑made DB/session/contexts and factory helpers

This page shows how to enable them and use them effectively.

Quickstart

Prerequisites:

  • Docker running locally (for Testcontainers)
  • pytest installed in your environment

Enable fixtures in your test suite via conftest.py:

# conftest.py
pytest_plugins = [
    "kwik.testing.fixtures.core_fixtures",
    "kwik.testing.fixtures.factories",
]

That makes these fixtures available:

  • postgres, settings, engine (session-scoped, DB lifecycle)
  • session (per-test, rollback isolation)
  • admin_user, regular_user
  • admin_context, no_user_context
  • user_factory, role_factory, permission_factory

Tip: You can override any fixture locally in your project’s conftest.py if you need to customize behavior.

For a full, in-depth explanation of each fixture (scope, lifecycle, parameters, and override patterns), see the dedicated page: Testing → Fixtures.

Database setup (Testcontainers)

core_fixtures starts a disposable PostgreSQL 15 container using Testcontainers and wires BaseKwikSettings to point to it. Schema is created at the start of the test session and dropped at the end. Tests run against a real database, not mocks.

If you prefer an existing DB, override settings and/or engine in your conftest.py to point at your instance.

Scenario: fluent test data

Scenario lets you declare users, roles, and permissions with a chainable API and builds them in the correct order.

from kwik.testing import Scenario

def test_business_flow(session, admin_user):
    scenario = (
        Scenario()
        .with_role("editor", permissions=["posts:read", "posts:write"])
        .with_user(name="john", email="john@test.com", roles=["editor"]) 
        .with_admin_user(name="alice")
        .build(session=session, admin_user=admin_user)
    )

    john = scenario.users["john"]
    editor = scenario.roles["editor"]
    assert john.id is not None
    assert editor.name == "editor"

Notes:

  • Supplying admin_user to build(...) is required when your scenario involves roles, permissions, or admin users.
  • The result gives you dictionaries for users, roles, and permissions by name.

Factories: quick entities in tests

Factory fixtures wrap the Scenario API for concise creation:

def test_role_and_user(user_factory, role_factory):
    role = role_factory(name="editor", permissions=["posts:read"]) 
    user = user_factory(name="john", roles=["editor"]) 

    assert role.name == "editor"
    assert user.email.endswith("@test.com")
  • user_factory(...) returns a User
  • role_factory(...) returns a Role
  • permission_factory(name=...) returns a Permission

These use admin_context automatically for operations that require elevated privileges.

IdentityAwareTestClient: authenticated requests

Wrap FastAPI’s TestClient to make requests “as a user” without manually handling tokens. The client mints a bearer token for the user from the application settings, opens an auth session when the application has an auth session store, and caches the token per user. It posts the login form only in the deprecated fallback described below. When the app's store is wired through the _get_auth_session_store override rather than Kwik(auth_session_store=...), pass auth_session_store= to IdentityAwareTestClient; otherwise it mints tokens without a session and every request is 401.

from fastapi.testclient import TestClient
from kwik.testing import IdentityAwareTestClient

def test_protected_endpoint(app, settings, admin_user):
    with TestClient(app) as http:
        client = IdentityAwareTestClient(http, settings)

        # Perform request as admin_user
        res = client.get_as(admin_user, "/api/v1/protected")
        assert res.status_code == 200

Settings come from the settings argument or, when you omit it, from app.state.settings, which the application sets when its lifespan runs (enter the TestClient as a context manager). The auth session store comes from the auth_session_store argument or from app.state.auth_session_store, which Kwik sets when you pass a store. An application without a store gets a token without a session, as in kwik 1.13.

token_for(user) returns the token itself, for example to call the API with another HTTP client:

token = client.token_for(admin_user)
headers = {"Authorization": f"Bearer {token}"}

A user object only needs an id: the token is not checked against the database until a request uses it, and a request is then refused with 401 if the account is inactive.

Deprecated fallback

If neither the settings argument nor app.state.settings is available, the client posts the password form to /api/v1/login/access-token and emits a DeprecationWarning. This fallback is removed in kwik 2.0; pass settings or enter the TestClient context instead.

Available methods mirror HTTP verbs: get_as, post_as, put_as, patch_as, delete_as. For public endpoints, use the wrapped client’s regular methods (e.g., client.get("/public")).

Tips and best practices

  • Keep fixtures lean and override locally when you need specific data or lifecycle changes.
  • Use factories for readability and to avoid duplicating setup steps across tests.
  • Avoid magic numbers/constants in tests to satisfy ruff rules; use named variables or fixtures.
  • For faster local feedback: pytest --disable-warnings --tb=short or run by markers (e.g., -m "not slow").
  • Parallel runs: pytest (defaults to parallel in this repo); disable with -n 0 when debugging.
  • Settings reference: kwik.settings.BaseKwikSettings
  • Testing utilities: kwik.testing (Scenario, IdentityAwareTestClient, fixtures)