Testing Support¶
Tortoise ORM provides testing utilities designed for pytest with true test isolation. Each test gets its own database context, ensuring tests don’t interfere with each other.
Quick Start¶
Create a
conftest.pyfile in your tests directory:
import os
import pytest_asyncio
from tortoise.contrib.test import tortoise_test_context
@pytest_asyncio.fixture
async def db():
"""Provide isolated database context for each test."""
db_url = os.getenv("TORTOISE_TEST_DB", "sqlite://:memory:")
async with tortoise_test_context(["myapp.models"], db_url=db_url) as ctx:
yield ctx
Write your tests as async functions:
import pytest
from myapp.models import User
@pytest.mark.asyncio
async def test_create_user(db):
user = await User.create(name="Test User", email="test@example.com")
assert user.id is not None
assert user.name == "Test User"
@pytest.mark.asyncio
async def test_filter_users(db):
await User.create(name="Alice")
await User.create(name="Bob")
users = await User.filter(name="Alice")
assert len(users) == 1
assert users[0].name == "Alice"
Run your tests:
pytest tests/ -v
tortoise_test_context Reference¶
The tortoise_test_context function creates an isolated ORM context for testing:
from tortoise.contrib.test import tortoise_test_context
async with tortoise_test_context(
modules=["myapp.models"], # Required: List of model modules
db_url="sqlite://:memory:", # Optional: Database URL (default: sqlite://:memory:)
app_label="models", # Optional: App label (default: "models")
connection_label="default", # Optional: Connection alias (default: "default")
) as ctx:
# Your test code here
pass
Parameters:
modules(list): List of module paths containing your models. Required.db_url(str): Database connection URL. Defaults tosqlite://:memory:.app_label(str): Label for the app in the ORM registry. Defaults to"models".connection_label(str): Alias for the database connection. Defaults to"default".
The context manager:
Creates a fresh
TortoiseContextInitializes the ORM with the given configuration
Generates database schemas
Yields the context for your test
Closes all connections on exit
Testing with Multiple Databases¶
For tests that require multiple database connections:
import pytest_asyncio
from tortoise.context import TortoiseContext
@pytest_asyncio.fixture
async def multi_db():
"""Fixture for testing with multiple databases."""
async with TortoiseContext() as ctx:
await ctx.init(config={
"connections": {
"primary": "sqlite://:memory:",
"secondary": "sqlite://:memory:",
},
"apps": {
"models": {
"models": ["myapp.models"],
"default_connection": "primary",
},
"archive": {
"models": ["myapp.archive_models"],
"default_connection": "secondary",
}
}
})
await ctx.generate_schemas()
yield ctx
Event Loop Isolation¶
Some backends (asyncpg, aiomysql) bind connection pools to the event loop that created
them. tortoise_test_context() handles this transparently – if the event loop changes
between tests, connections are automatically recreated.
This means you don’t need loop_scope="session" or any special pytest-asyncio
configuration. The simplest setup works:
# pyproject.toml -- no loop_scope overrides needed
[tool.pytest.ini_options]
asyncio_mode = "auto"
If you use TortoiseContext directly (without tortoise_test_context), you may see
a TortoiseLoopSwitchWarning when the loop changes. Suppress it with:
import warnings
from tortoise.warnings import TortoiseLoopSwitchWarning
warnings.filterwarnings("ignore", category=TortoiseLoopSwitchWarning)
Unit Testing Without a Database¶
For testing pure business logic that reads model attributes and iterates relations
without making queries, use Model.construct() to create model instances in memory:
from myapp.models import User, Organization, Membership
def test_user_has_active_membership():
org = Organization.construct(id=1, name="Corp")
membership = Membership.construct(
organization=org,
role="admin",
is_active=True,
)
user = User.construct(
id=1,
email="test@example.com",
memberships=[membership],
)
# Pure business logic -- no database needed
active = [m for m in user.memberships if m.is_active]
assert len(active) == 1
assert active[0].role == "admin"
construct() creates “detached” instances that behave like ORM-loaded objects:
Reverse FK fields (e.g.,
user.memberships) accept lists and wrap them inReverseRelation, solen(),in, iteration, andbool()all work.M2M fields work the same way, wrapped in
ManyToManyRelation.FK fields populate the source field automatically (e.g.,
event.tournament_idis set fromtournament.pk).No validation is performed – null checks, type checks, and
_saved_in_dbguards are all skipped.
Note
construct() requires Tortoise to be initialized (via tortoise_test_context
or Tortoise.init()) for relation fields to work, because relation metadata is
resolved during initialization. For simple data-only fields, it works without
initialization.
See tortoise.models.Model.construct() for the full API reference.
Testing Database Capabilities¶
Use requireCapability to skip tests based on database capabilities:
from tortoise.contrib.test import requireCapability
@pytest.mark.asyncio
@requireCapability(dialect="postgres")
async def test_postgres_specific_feature(db):
"""This test only runs on PostgreSQL."""
# Test postgres-specific functionality
pass
@pytest.mark.asyncio
@requireCapability(dialect="sqlite")
async def test_sqlite_specific_feature(db):
"""This test only runs on SQLite."""
pass
Environment Variables¶
Configure your test database via environment variables:
# SQLite (default)
export TORTOISE_TEST_DB="sqlite://:memory:"
# PostgreSQL
export TORTOISE_TEST_DB="postgres://user:pass@localhost:5432/testdb"
# MySQL
export TORTOISE_TEST_DB="mysql://user:pass@localhost:3306/testdb"
Using {} in the URL creates randomized database names (useful for parallel testing):
export TORTOISE_TEST_DB="sqlite:///tmp/test-{}.sqlite"
export TORTOISE_TEST_DB="postgres://user:pass@localhost:5432/test_{}"
Utility Functions¶
truncate_all_models¶
Truncate all model tables in the current context. The function handles foreign key constraints automatically:
PostgreSQL: Uses a single
TRUNCATE ... CASCADEstatement (fast, single round-trip).Other databases: Deletes in topological order — child tables are emptied before the parent tables they reference, avoiding FK constraint violations.
from tortoise.contrib.test import truncate_all_models
@pytest.mark.asyncio
async def test_with_truncation(db):
# Create some data
await User.create(name="Test")
# Truncate all tables (FK-safe)
await truncate_all_models()
# Tables are now empty
count = await User.all().count()
assert count == 0
Migration from Legacy Test Classes¶
If you’re upgrading from the legacy test.TestCase classes, see the
Migration Guide: Tortoise 1.0 for detailed migration instructions.
Quick reference:
Legacy (Removed) |
Modern Replacement |
|---|---|
|
pytest + |
|
pytest + |
|
pytest + |
|
pytest + |
|
|
|
(automatic with context manager) |
|
|
|
|
|
|
Reference¶
Modern testing utilities for Tortoise ORM.
Use tortoise_test_context() with pytest fixtures:
@pytest_asyncio.fixture async def db():
- async with tortoise_test_context([“myapp.models”]) as ctx:
yield ctx
@pytest.mark.asyncio async def test_example(db):
user = await User.create(name=”Test”) assert user.id is not None
For capability-based test skipping:
@requireCapability(dialect=”sqlite”) @pytest.mark.asyncio async def test_sqlite_only(db):
# This test only runs on SQLite …
-
tortoise.contrib.test.requireCapability(connection_name=
'models', **conditions)[source]¶ Skip a test if the required capabilities are not matched.
Note
The database must be initialized before the decorated test runs.
Usage:
@requireCapability(dialect='sqlite') @pytest.mark.asyncio async def test_run_sqlite_only(db): ...Or to conditionally skip a class:
@requireCapability(dialect='sqlite') class TestSqlite: @pytest.mark.asyncio async def test_something(self, db): ...
-
tortoise.contrib.test.tortoise_test_context(modules, db_url=
'sqlite://:memory:', app_label='models', *, connection_label=None, use_tz=True, timezone='UTC', routers=None)[source]¶ Async context manager for isolated test database setup.
This is the recommended way to set up Tortoise ORM for testing with pytest. Each call creates a completely isolated context with its own: - ConnectionHandler (no global state pollution) - Apps registry - Database (created fresh, cleaned up on exit) - Timezone and router configuration
- Example with pytest-asyncio:
@pytest_asyncio.fixture async def db():
- async with tortoise_test_context([“myapp.models”]) as ctx:
yield ctx
@pytest.mark.asyncio async def test_create_user(db):
user = await User.create(name=”Alice”) assert user.id is not None
Features: - Creates isolated TortoiseContext (no global state pollution) - Creates fresh database and generates schemas - Cleans up connections on exit - xdist-safe (each worker gets own context)
- Args:
modules: List of module paths to discover models from. db_url: Database URL, defaults to in-memory SQLite. app_label: The app label for the models, defaults to “models”. connection_label: The connection alias name. If None, defaults to “default”. use_tz: If True, datetime fields will be timezone-aware. timezone: Timezone to use, defaults to “UTC”. routers: List of database router paths or classes.
- Yields:
An initialized TortoiseContext ready for use.
- Return type:¶
AsyncIterator
- async tortoise.contrib.test.truncate_all_models()[source]¶
Truncate all models in the current context.
This is a utility function for test cleanup that deletes all rows from all registered model tables.
On PostgreSQL, uses
TRUNCATE ... CASCADEfor a single fast statement. On other databases, deletes in topological (FK dependency) order so that child rows are removed before parent rows they reference.- Raises:
ValueError: If Tortoise.apps is not loaded.
- Return type:¶
None