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

  1. Create a conftest.py file 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
  1. 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"
  1. 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 to sqlite://: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:

  1. Creates a fresh TortoiseContext

  2. Initializes the ORM with the given configuration

  3. Generates database schemas

  4. Yields the context for your test

  5. 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 in ReverseRelation, so len(), in, iteration, and bool() all work.

  • M2M fields work the same way, wrapped in ManyToManyRelation.

  • FK fields populate the source field automatically (e.g., event.tournament_id is set from tournament.pk).

  • No validation is performed – null checks, type checks, and _saved_in_db guards 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 ... CASCADE statement (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:

Migration Mapping

Legacy (Removed)

Modern Replacement

test.TestCase

pytest + db fixture

test.IsolatedTestCase

pytest + db fixture (isolation is default)

test.TruncationTestCase

pytest + db fixture + truncate_all_models()

test.SimpleTestCase

pytest + db fixture

initializer()

tortoise_test_context()

finalizer()

(automatic with context manager)

self.assertEqual(a, b)

assert a == b

self.assertIn(a, b)

assert a in b

self.assertRaises(Exc)

pytest.raises(Exc)

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):
        ...
Parameters:
connection_name='models'

name of the connection to retrieve capabilities from.

**conditions

capability tests which must all pass for the test to run.

Return type:

Callable

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 ... CASCADE for 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