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.
- 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/ -vThe 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
passParameters:
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
TortoiseContext - Initializes the ORM with the given configuration
- Generates database schemas
- Yields the context for your test
- Closes all connections on exit
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 ctxSome 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)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."""
passConfigure 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_{}"Truncate all model tables in the current context:
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
await truncate_all_models()
# Tables are now empty
count = await User.all().count()
assert count == 0If you're upgrading from the legacy test.TestCase classes, see the
:ref:`migration_guide` for detailed migration instructions.
Quick reference:
| 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) |
.. automodule:: tortoise.contrib.test
:members: tortoise_test_context, truncate_all_models, requireCapability
:show-inheritance: