Testing
OpenViper TestKit is the pytest-based testing layer for OpenViper
projects. It lives in openviper.testing and is exposed as the
pytest-openviper plugin entry point.
The TestKit is designed to feel like normal pytest: application startup, HTTP clients, database setup, settings overrides, and common framework test doubles are provided through fixtures and small helper functions.
Overview
OpenViper applications are async-first, so tests usually need an async HTTP client, predictable application lifespan handling, and isolated state for database and framework side effects. TestKit provides:
pytest fixtures for apps, clients, databases, users, auth, and service doubles.
An async HTTP client that calls the ASGI app without a live server.
Test database configuration with safety checks and reset helpers.
Model factories for building and creating test data.
Authentication helpers for bearer tokens, forced authentication, roles, and permissions.
Assertion helpers for HTTP responses, JSON payloads, validation errors, model state, OpenAPI schemas, events, tasks, mail, cache, and snapshots.
Multi-database test configuration and alias tracking.
Standalone helper functions that can be used outside the fixture mechanism.
For request and response behavior, see HTTP - Requests, Responses & Views. For model and database behavior, see Database & ORM.
Installation and Setup
Install the testing extra to pull in pytest and related dependencies:
pip install openviper[testing]
This installs pytest, pytest-asyncio, and httpx alongside OpenViper.
The testing utilities are also available without the extra if these packages
are already installed, but the extra ensures compatible versions are present.
When OpenViper is installed with its pytest entry point, pytest can discover
the plugin automatically. Projects may also enable it explicitly in
tests/conftest.py:
pytest_plugins = ["openviper.testing.plugin"]
The plugin registers OpenViper markers and makes the fixtures from
openviper.testing.fixtures available to tests.
Configuration
Configure TestKit in pyproject.toml under [tool.openviper.testing]:
[tool.openviper.testing]
app = "myproject.main:app"
settings = "myproject.settings.testing"
database_url = "sqlite+aiosqlite:///:memory:"
database_isolation = "transaction"
migrate = true
app is required unless OPENVIPER_TEST_APP is set. It may point to an
OpenViper instance or a zero-argument app factory.
Available options:
Option |
Description |
|---|---|
|
Import path to the OpenViper app instance or factory. |
|
Optional settings module loaded through |
|
Explicit test database URL. When omitted, a dedicated |
|
One of |
|
When true, creates registered tables before each database fixture. |
|
When true, loads the configured test settings module. |
|
When true, patches email delivery to prevent real sends. |
|
When true, patches task enqueuing to prevent real broker calls. |
|
When false (default), the real cache backend is used unless the
|
The OPENVIPER_TEST_APP environment variable overrides the app option.
OpenViperTestConfig is the resolved configuration dataclass. It is
available as the openviper_test_config session-scoped fixture.
OpenViperTestingConfigError is raised for invalid configuration.
Core Fixtures
appImports the configured OpenViper application, enables debug mode, rebuilds middleware, and runs startup and shutdown handlers around the test.
clientProvides an
httpx.AsyncClientbound to the app through ASGI transport. It supports regular HTTP methods, JSON payloads, query params, headers, cookies, and redirects.dbConfigures the test database, creates registered tables when
migrateis enabled, and resets state after the test.transactional_dbUses the recreate reset path for tests that need committed data or behavior spanning multiple database connections.
migrated_dbandisolated_dbExplicit variants that force migrations and database recreation.
setup_test_databaseSession-scoped fixture that migrates once and keeps the engine alive for the entire test session. Yields a
SessionDatabasehandle.override_settingsTemporarily replaces OpenViper’s frozen settings object. Overrides are restored at test cleanup even if the test fails.
async def test_feature_flag(client, override_settings):
override_settings(DEBUG=False)
response = await client.get("/")
assert response.status_code in {200, 404}
Testing Routes
Use client for route tests. No live server is started.
async def test_homepage(client):
response = await client.get("/")
assert response.status_code == 200
JSON requests work through the usual httpx interface:
async def test_create_user(client):
response = await client.post(
"/users",
json={
"email": "user@example.com",
"name": "Test User",
},
)
assert response.status_code == 201
Testing the Database
The database fixtures configure OpenViper’s async engine with the configured
test database URL. TestKit rejects empty URLs and production-looking database
names such as prod, production, main, and live.
async def test_create_user(db):
user = await User.objects.create(email="user@example.com")
found = await User.objects.get_or_none(id=user.id)
assert found is not None
Isolation strategies:
transactionFast default strategy. In the current implementation it resets registered metadata after the test because OpenViper ORM operations may use their own connections.
truncateDeletes rows from registered tables without dropping metadata.
recreateDrops and recreates registered tables. This is slower but useful for integration tests.
in_memoryForces
sqlite+aiosqlite:///:memory:for small, local test suites.
Use migrate_database() and truncate_database() from
openviper.testing.database when a test needs to control those operations
manually.
resolve_test_database_url() resolves a database URL from config values.
in_memory isolation always uses SQLite in-memory, and an explicit
database_url from the testing config is used verbatim. Otherwise the
project’s settings.DATABASES default URL is used with its database name
rewritten to a dedicated test_-prefixed database (e.g. shop becomes
test_shop), so tests never touch the project’s real database. When no
URL is configured it defaults to SQLite in-memory.
assert_safe_database_url() rejects empty or production-looking database
URLs and warns on SQLite file databases whose name does not contain test.
Multi-Database Testing
openviper.testing.database provides helpers for projects that use
multiple database aliases:
Model Factories
Factories build unsaved model instances or create saved records.
from openviper.testing.factories import LazyAttribute, ModelFactory, Sequence
class UserFactory(ModelFactory[User]):
class Meta:
model = User
email = Sequence(lambda index: f"user{index}@example.com")
name = LazyAttribute(lambda values: values["email"].split("@")[0])
async def test_user_factory(db):
user = await UserFactory.create()
assert user.email.startswith("user")
Available factory helpers:
build(**overrides)returns an unsaved instance.create(**overrides)saves the instance withignore_permissions=True.build_batch(size, **overrides)andcreate_batch(size, **overrides)create multiple instances.Sequencegenerates incrementing values viaitertools.count.LazyAttributederives values from attributes already evaluated.RelatedFactorybuilds a related object from another factory.PostGenerationruns a callback aftercreate.
Pre-built factories:
UserFactorybuilds the active user model (supports custom user models). Passpassword="raw"tocreate()to set a hashed password.SuperuserFactoryextendsUserFactorywithis_staffandis_superuserset toTrue.PermissionFactorybuildsPermissionrecords.RoleFactorybuildsRolerecords.
Authentication Helpers
The plugin provides simple user-like fixtures and authenticated clients:
userA
TestUserobject withid,pk,email,permissions, androles.admin_userA staff/superuser variant with an
adminrole andadmin.accesspermission.user_factoryA callable that creates
TestUserinstances with auto-incrementing IDs and emails.auth_clientandadmin_clientClients with a bearer token attached for
useroradmin_user. These use stub users without database records.db_useranddb_admin_userCreate real
Userand superuser records in the test database viaUserFactoryandSuperuserFactory.authenticated_clientandadmin_authenticated_clientClients with bearer tokens backed by real database user records. Use these when route handlers perform a database lookup from the JWT
subclaim.
Helper functions:
token_for_user(user)creates a JWT access token.force_authenticate(client, user)attaches a bearer token to a client.attach_bearer_token(client, token)attaches an explicit token.attach_session_cookie(client, value)attaches a session cookie.login_user(client, path, **credentials)posts credentials to a login route.with_permissions(user, permissions)andwith_roles(user, roles)set test-only permission metadata in-place.
from openviper.testing.auth import force_authenticate
async def test_dashboard_allows_user(client, user):
force_authenticate(client, user)
response = await client.get("/dashboard")
assert response.status_code == 200
Settings Overrides
override_openviper_settings(**overrides) is a context manager that
temporarily replaces OpenViper’s frozen settings object. It validates field
names against the Settings dataclass and raises
OpenViperTestingConfigError for unknown fields.
override_settings(**overrides) is a decorator form that works on sync
functions, async functions, and test classes (wrapping every method whose name
begins with test).
from openviper.testing.settings import override_settings
@override_settings(DEBUG=False, ALLOWED_HOSTS=("testserver",))
async def test_secure_mode(client):
response = await client.get("/")
assert response.status_code != 500
Assertion Helpers
HTTP and JSON helpers live in openviper.testing.assertions:
assert_status(response, expected)assert_header(response, name, expected=None)assert_cookie(response, name)assert_redirects(response, expected_location=None)assert_response_json(response, expected)assert_json_contains(response, expected)assert_json_path(payload, path, expected)
Validation and model helpers:
assert_validation_error(response, field)assert_field_error(response, field)assert_error_code(response, code)assert_model_exists(Model, **filters)assert_model_count(Model, expected, **filters)assert_queryset_count(queryset, expected)assert_field_value(instance, field, expected)
from openviper.testing.assertions import assert_status, assert_validation_error
async def test_create_user_requires_email(client):
response = await client.post("/users", json={})
assert_status(response, 422)
assert_validation_error(response, "email")
Testing Mail, Events, Tasks, Cache, and Storage
TestKit includes lightweight service doubles for common side effects:
mailoutboxA list that captures
TestEmailrecords. Patchessend_nowand suppresses background delivery.event_recorderRecords named events and payloads. Use
assert_event_emitted(),assert_event_count(), andassert_event_payload().task_queueandtask_runnerCapture queued tasks with
TaskQueueor run sync/async callables immediately withEagerTaskRunner.cacheandclear_cacheProvide an isolated async in-memory cache.
clear_cacheis a callable that clears the cache.tmp_storageanduploaded_fileProvide a temporary storage root and in-memory
UploadFileobjects.snapshotProvides optional filesystem-backed snapshot assertions.
Each fixture has a corresponding standalone helper that can be used outside the pytest fixture mechanism:
create_mailoutbox()returns(outbox, patches).create_event_recorder()returns(recorder, patches).create_task_queue()returns(queue, patches).setup_test_cache()returns(instance, restore).
from openviper.testing.mail import InMemoryMailBackend, assert_email_count
async def test_welcome_email(mailoutbox):
backend = InMemoryMailBackend(mailoutbox)
await backend.send("Welcome", ["user@example.com"])
assert_email_count(mailoutbox, 1)
Testing OpenAPI and Admin
openapi_schema returns the app’s generated OpenAPI document. The
openviper.testing.openapi module provides:
assert_openapi_path(schema, path)assert_openapi_operation(schema, path, method)assert_request_schema(schema, path, method)assert_response_schema(schema, path, method, status_code)
from openviper.testing.openapi import assert_openapi_path
def test_openapi_has_users_endpoint(openapi_schema):
assert_openapi_path(openapi_schema, "/users")
Use admin_client for admin route tests that need an authenticated
admin-like client.
CLI Testing
cli_runner returns a Click CliRunner for isolated command tests.
The helper assert_exit_code(result, expected=0) checks command results
with a useful failure message.
from openviper.testing.cli import assert_exit_code
def test_command(cli_runner):
result = cli_runner.invoke(["--help"])
assert_exit_code(result, 0)
Pytest Markers
The plugin registers these markers:
Marker |
Purpose |
|---|---|
|
Tests using OpenViper testing features. |
|
Tests requiring database access. |
|
Tests requiring committed data or real transaction behavior. |
|
Broader integration tests. |
|
Slow tests. |
|
Admin UI or admin API tests. |
|
OpenAPI schema tests. |
|
Authentication or authorization tests. |
Public API Reference
The openviper.testing package exports the following symbols:
Symbol |
Module |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Additional public symbols not re-exported from the package root:
resolve_test_database_url,assert_safe_database_url(openviper.testing.database)OpenViperTestingConfigError,load_testing_config,load_app,import_from_path(openviper.testing.settings)TestUser(openviper.testing.fixtures)TestEmail,InMemoryMailBackend(openviper.testing.mail)EventRecorder,RecordedEvent(openviper.testing.events)TaskQueue,QueuedTask,EagerTaskRunner(openviper.testing.tasks)TestCache(openviper.testing.cache)
Project Scaffold
Generate a minimal test setup with:
openviper test init
The command creates:
tests/conftest.pywithpytest_plugins = ["openviper.testing.plugin"].tests/test_health.pywith a basic async health check.[tool.openviper.testing]inpyproject.tomlwhen missing.
Existing files are skipped unless --force is passed.
Running Tests
The openviper test command runs the project test suite through pytest:
openviper test
Supported flags:
-v/--verboseIncrease output verbosity. Pass twice for maximum detail.
-x/--failfastStop on the first test failure.
--create-dbForce creation of the test database even if it already exists. Sets the
OPENVIPER_TEST_CREATE_DB=1environment variable so that the database fixtures run migrations regardless of the current state.--reuse-db/--keepdbReuse the existing test database instead of dropping and recreating it between test runs. Sets
OPENVIPER_TEST_REUSE_DB=1so that the database fixtures skip teardown and re-migration.--keepdbis an alias for--reuse-db.
These flags are passed through to the test runner as environment variables
that the database fixtures in openviper.testing.database read at setup time.
Best Practices
Use a dedicated test settings module and test database URL.
Prefer in-memory SQLite for small library tests and explicit test database URLs for integration suites.
Keep route tests focused on request/response behavior.
Use factories for model setup instead of copying object creation into every test.
Use
override_settingsfor feature flags and per-test configuration.Use
authenticated_clientfor routes that load the user from the database; useauth_clientwhen only the token signature is verified.Assert framework side effects through
mailoutbox,event_recorder,task_queue,cache, andtmp_storagerather than real external services.Use the standalone helpers (
create_mailoutbox,create_event_recorder,create_task_queue,setup_test_cache) when writing unit tests outside the pytest fixture mechanism.Do not use production database, mail, task, cache, or storage backends in tests.
Limitations
This release provides the core TestKit surface and lightweight helpers. The following areas are intentionally limited:
True nested transaction rollback is not guaranteed for all ORM operations, because OpenViper database calls may acquire independent connections. TestKit therefore resets metadata or truncates rows for deterministic cleanup.
The built-in
userandadmin_userfixtures areTestUserobjects, not real ORM records. Usedb_useranddb_admin_userwhen the route handler loads the user from the database.Mail, task, cache, event, and storage doubles do not automatically replace every possible project-specific backend. Wire them into custom services through fixtures when needed.
Snapshot testing is optional and intentionally small.
Advanced pytest-xdist database provisioning is planned but not implemented as a stable public API.