Multi-Backend Testing¶
RestMachine ORM includes a testing framework that lets you write tests once and run them against all backends automatically.
Architecture¶
The testing framework follows Dave Farley's 4-layer testing architecture:
┌─────────────────────────────────────┐
│ Test Layer │
│ (test methods) │
└──────────────┬──────────────────────┘
│
┌──────────────▼──────────────────────┐
│ DSL Layer │
│ (backend-agnostic operations) │
└──────────────┬──────────────────────┘
│
┌──────────────▼──────────────────────┐
│ Driver Layer │
│ (backend-specific implementations) │
└──────────────┬──────────────────────┘
│
┌──────────────▼──────────────────────┐
│ System Under Test │
│ (RestMachine ORM) │
└─────────────────────────────────────┘
Quick Example¶
from typing import List, Type
from restmachine_orm import Model, Field
from restmachine_orm_testing import MultiBackendTestBase
class User(Model):
id: str = Field(primary_key=True)
email: str
name: str
class TestUserModel(MultiBackendTestBase):
def get_test_models(self) -> List[Type]:
return [User]
def test_create_user(self, orm):
"""This test runs against ALL enabled backends."""
orm_client, backend_name = orm
user = orm_client.create_and_verify(
User,
id="user-123",
email="alice@example.com",
name="Alice"
)
assert user.id == "user-123"
assert user.email == "alice@example.com"
When you run this test, it automatically runs against all enabled backends (by default, just inmemory
).
Writing Tests¶
Basic Structure¶
from restmachine_orm_testing import MultiBackendTestBase
from typing import List, Type
class TestYourFeature(MultiBackendTestBase):
def get_test_models(self) -> List[Type]:
"""Return models used in tests."""
return [User, TodoItem]
def test_something(self, orm):
"""Each test receives orm fixture."""
orm_client, backend_name = orm
# Use orm_client DSL here
Enabling Multiple Backends¶
class TestUserCRUD(MultiBackendTestBase):
ENABLED_BACKENDS = ['inmemory', 'dynamodb']
def get_test_models(self) -> List[Type]:
return [User]
def test_create_user(self, orm):
# Runs twice: once for InMemory, once for DynamoDB
orm_client, backend_name = orm
user = orm_client.create_and_verify(User, id="user-1", name="Alice")
Skip Specific Backends¶
from restmachine_orm_testing import skip_backend, only_backends
class TestFeatures(MultiBackendTestBase):
@skip_backend('dynamodb')
def test_memory_only(self, orm):
"""Skipped for DynamoDB."""
pass
@only_backends('inmemory')
def test_only_inmemory(self, orm):
"""Only runs for InMemory."""
pass
DSL Methods¶
The DSL provides backend-agnostic operations:
Create¶
# Create and verify
user = orm_client.create_and_verify(
User,
id="user-123",
email="alice@example.com",
name="Alice"
)
# Expect creation to fail
from restmachine_orm.backends.base import DuplicateKeyError
orm_client.expect_create_failure(
User,
DuplicateKeyError,
id="user-123", # Duplicate
email="different@example.com"
)
Read¶
# Get and verify exists
user = orm_client.get_and_verify_exists(User, id="user-123")
# Verify doesn't exist
orm_client.get_and_verify_not_exists(User, id="nonexistent")
Update¶
# Update and verify
updated = orm_client.update_and_verify(
user,
age=31,
name="Alice Smith"
)
# Expect update to fail
from restmachine_orm.backends.base import NotFoundError
nonexistent = User(id="fake", email="fake@example.com", name="Fake")
orm_client.expect_update_failure(
nonexistent,
NotFoundError,
age=25
)
Delete¶
Upsert¶
# Upsert and verify
user = orm_client.upsert_and_verify(
User,
id="user-123",
email="alice.new@example.com",
name="Alice"
)
Query¶
# Query with filters
result = orm_client.query_models(
User,
filters={"age__gte": 25, "age__lte": 50"},
order_by=["age"],
limit=10
)
assert result.success
users = result.data
# Query and verify count
users = orm_client.query_and_verify_count(
User,
expected_count=3,
filters={"age__gte": 30}
)
# Count matching records
count = orm_client.count_models(User, age__gte=30)
# Check if exists
exists = orm_client.model_exists(User, email="alice@example.com")
# Get all
all_users = orm_client.all_models(User)
Complete Example¶
from typing import List, Type
from datetime import datetime
from restmachine_orm import Model, Field
from restmachine_orm.backends.base import DuplicateKeyError, NotFoundError
from restmachine_orm_testing import MultiBackendTestBase
class User(Model):
id: str = Field(primary_key=True)
email: str = Field(unique=True)
name: str
age: int = Field(ge=0, default=0)
created_at: datetime | None = Field(None, auto_now_add=True)
class TestUserModel(MultiBackendTestBase):
ENABLED_BACKENDS = ['inmemory', 'dynamodb']
def get_test_models(self) -> List[Type]:
return [User]
def test_create_user(self, orm):
"""Test creating a user."""
orm_client, backend_name = orm
user = orm_client.create_and_verify(
User,
id="user-123",
email="alice@example.com",
name="Alice",
age=30
)
assert user.id == "user-123"
assert user.email == "alice@example.com"
assert user.created_at is not None
def test_create_duplicate_fails(self, orm):
"""Test duplicate creation fails."""
orm_client, backend_name = orm
orm_client.create_and_verify(
User,
id="user-123",
email="test@example.com",
name="Test"
)
orm_client.expect_create_failure(
User,
DuplicateKeyError,
id="user-123",
email="different@example.com",
name="Different"
)
def test_get_user(self, orm):
"""Test retrieving a user."""
orm_client, backend_name = orm
# Create user
orm_client.create_and_verify(
User,
id="user-123",
email="alice@example.com",
name="Alice"
)
# Get user
user = orm_client.get_and_verify_exists(User, id="user-123")
assert user.email == "alice@example.com"
# Verify nonexistent
orm_client.get_and_verify_not_exists(User, id="nonexistent")
def test_update_user(self, orm):
"""Test updating a user."""
orm_client, backend_name = orm
user = orm_client.create_and_verify(
User,
id="user-123",
email="alice@example.com",
name="Alice",
age=30
)
updated = orm_client.update_and_verify(
user,
age=31,
name="Alice Smith"
)
assert updated.age == 31
assert updated.name == "Alice Smith"
def test_delete_user(self, orm):
"""Test deleting a user."""
orm_client, backend_name = orm
user = orm_client.create_and_verify(
User,
id="user-123",
email="alice@example.com",
name="Alice"
)
orm_client.delete_and_verify(user)
orm_client.get_and_verify_not_exists(User, id="user-123")
def test_upsert_user(self, orm):
"""Test upserting a user."""
orm_client, backend_name = orm
# Initial upsert (creates)
user1 = orm_client.upsert_and_verify(
User,
id="user-123",
email="alice@example.com",
name="Alice",
age=30
)
assert user1.age == 30
# Second upsert (updates)
user2 = orm_client.upsert_and_verify(
User,
id="user-123",
email="alice.new@example.com",
name="Alice Jones",
age=31
)
assert user2.age == 31
assert user2.email == "alice.new@example.com"
def test_query_users(self, orm):
"""Test querying users."""
orm_client, backend_name = orm
# Create test data
orm_client.create_and_verify(User, id="user-1", email="a@example.com", name="Alice", age=30)
orm_client.create_and_verify(User, id="user-2", email="b@example.com", name="Bob", age=25)
orm_client.create_and_verify(User, id="user-3", email="c@example.com", name="Carol", age=35)
# Query with filters
users = orm_client.query_and_verify_count(
User,
expected_count=2,
filters={"age__gte": 30}
)
ages = [u.age for u in users]
assert 30 in ages
assert 35 in ages
def test_count_and_exists(self, orm):
"""Test counting and existence checks."""
orm_client, backend_name = orm
# Create users
orm_client.create_and_verify(User, id="user-1", email="a@example.com", name="Alice", age=30)
orm_client.create_and_verify(User, id="user-2", email="b@example.com", name="Bob", age=25)
# Count
count = orm_client.count_models(User, age__gte=30)
assert count == 1
# Exists
exists = orm_client.model_exists(User, email="a@example.com")
assert exists
not_exists = orm_client.model_exists(User, email="nonexistent@example.com")
assert not not_exists
Adding a New Backend¶
To add support for a new backend:
1. Create Driver¶
# In your backend package
from restmachine_orm_testing import DriverInterface, CreateOperation, OperationResult
class MyBackendDriver(DriverInterface):
def __init__(self, **config):
self.backend = MyBackend(**config)
def execute_create(self, operation: CreateOperation) -> OperationResult:
try:
self.setup_backend(operation.model_class)
instance = operation.model_class.create(**operation.data)
return OperationResult(
success=True,
instance=instance,
data=instance.model_dump()
)
except Exception as e:
return OperationResult(success=False, error=e)
# Implement other methods...
def get_backend_name(self) -> str:
return "mybackend"
def setup_backend(self, model_class):
model_class.model_backend = self.backend
2. Register Driver¶
In your package's conftest.py
:
import pytest
from restmachine_orm_testing import MultiBackendTestBase
from my_backend.testing import MyBackendDriver
# Register driver
original_create_driver = MultiBackendTestBase.create_driver
@classmethod
def patched_create_driver(cls, backend_name: str):
if backend_name == 'mybackend':
return MyBackendDriver(config_option="value")
return original_create_driver(backend_name)
MultiBackendTestBase.create_driver = patched_create_driver
3. Enable in Tests¶
class TestFeatures(MultiBackendTestBase):
ENABLED_BACKENDS = ['inmemory', 'mybackend']
def get_test_models(self):
return [User]
def test_something(self, orm):
# Now runs for both backends
pass
Benefits¶
- Write Once, Test Everywhere: Same test code for all backends
- Backend Consistency: Ensures all backends behave the same
- Easy Backend Addition: Just implement driver and register
- Confidence: New features work everywhere before release
- Maintainability: Changes to DSL apply to all backends
See Also¶
- Installation - Install backends
- DynamoDB Backend - Test with DynamoDB
- API Reference - Complete API documentation