perf: Optimize slow tests with shared tenant fixtures

- Add session-scoped shared_tenant and second_shared_tenant fixtures to conftest.py
- Refactor test_models.py and test_user_model.py to use shared fixtures
- Avoid ~40s migration overhead per tenant by reusing fixtures across tests
- Add pytest-xdist to dev dependencies for future parallel test execution

Previously 4 tests each created their own tenant (~40s each = ~160s total).
Now they share session-scoped tenants, reducing overhead significantly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-10 02:22:43 -05:00
parent 485f86086b
commit ba2c656243
4 changed files with 96 additions and 103 deletions

View File

@@ -146,6 +146,7 @@ dev = [
"pytest==9.0.1",
"pytest-django==4.11.1",
"pytest-sugar==1.1.1",
"pytest-xdist>=3.5.0",
"ruff==0.14.6",
"sphinx==8.2.3",
"sphinx-autobuild==2025.8.25",

View File

@@ -18,3 +18,56 @@ def user(db) -> User:
For unit tests, use create_mock_user() from factories.py instead.
"""
return UserFactory()
# =============================================================================
# Shared Tenant Fixtures (Session-scoped for performance)
# =============================================================================
# Creating a tenant in django-tenants runs all migrations (~40 seconds).
# These fixtures are session-scoped to avoid recreating tenants for each test.
_shared_tenant_cache = {}
@pytest.fixture(scope="session")
def shared_tenant(django_db_setup, django_db_blocker):
"""
Session-scoped tenant fixture for tests that need a real tenant.
This tenant is created ONCE per test session and reused across all tests.
Use this instead of creating tenants in individual tests to avoid the
~40 second migration overhead per tenant.
Usage:
@pytest.mark.django_db
def test_something(shared_tenant):
user = User(tenant=shared_tenant, ...)
"""
from smoothschedule.identity.core.models import Tenant
with django_db_blocker.unblock():
# Check if tenant already exists from a previous run (--reuse-db)
tenant = Tenant.objects.filter(schema_name="sharedtest").first()
if not tenant:
tenant = Tenant.objects.create(
name="Shared Test Business",
schema_name="sharedtest"
)
return tenant
@pytest.fixture(scope="session")
def second_shared_tenant(django_db_setup, django_db_blocker):
"""
Second session-scoped tenant for tests that need multiple tenants.
"""
from smoothschedule.identity.core.models import Tenant
with django_db_blocker.unblock():
tenant = Tenant.objects.filter(schema_name="sharedtest2").first()
if not tenant:
tenant = Tenant.objects.create(
name="Shared Test Business 2",
schema_name="sharedtest2"
)
return tenant

View File

@@ -341,45 +341,32 @@ class TestGetAccessibleTenants:
"""
Test get_accessible_tenants() method.
Note: These tests use database access because the method accesses
ForeignKey relationships which trigger database queries even with mocking.
Uses shared tenant fixtures (session-scoped) to avoid ~40s migration overhead
per tenant creation. See conftest.py for fixture definitions.
"""
@pytest.mark.django_db
def test_returns_all_tenants_for_platform_user(self):
# Arrange
from smoothschedule.identity.core.models import Tenant
import uuid
# Create a couple of tenants
unique_id1 = str(uuid.uuid4())[:8]
Tenant.objects.create(name=f"Tenant1 {unique_id1}", schema_name=f"tenant1{unique_id1}")
unique_id2 = str(uuid.uuid4())[:8]
Tenant.objects.create(name=f"Tenant2 {unique_id2}", schema_name=f"tenant2{unique_id2}")
def test_returns_all_tenants_for_platform_user(self, shared_tenant, second_shared_tenant):
# Arrange - use shared fixtures instead of creating new tenants
user = create_user_instance(User.Role.PLATFORM_MANAGER)
# Act
result = user.get_accessible_tenants()
# Assert
assert result.count() >= 2 # At least the two we created
# Assert - at least our two shared tenants exist
assert result.count() >= 2
@pytest.mark.django_db
def test_returns_single_tenant_for_tenant_user(self):
# Arrange
from smoothschedule.identity.core.models import Tenant
def test_returns_single_tenant_for_tenant_user(self, shared_tenant):
# Arrange - use shared fixture
import uuid
unique_id = str(uuid.uuid4())[:8]
tenant = Tenant.objects.create(name=f"My Business {unique_id}", schema_name=f"mybiz{unique_id}")
user = User(
username=f"owner{unique_id}",
email=f"owner{unique_id}@test.com",
role=User.Role.TENANT_OWNER,
tenant=tenant
tenant=shared_tenant
)
user.save()
@@ -388,7 +375,7 @@ class TestGetAccessibleTenants:
# Assert
assert result.count() == 1
assert result.first() == tenant
assert result.first() == shared_tenant
@pytest.mark.django_db
def test_returns_empty_queryset_for_tenant_user_without_tenant(self):
@@ -482,23 +469,16 @@ class TestSaveMethodValidation:
assert user.is_superuser is True
@pytest.mark.django_db
def test_clears_tenant_for_platform_users(self):
# Arrange
from smoothschedule.identity.core.models import Tenant
def test_clears_tenant_for_platform_users(self, shared_tenant):
# Arrange - use shared fixture to avoid ~40s migration overhead
import uuid
# Create a tenant first with unique schema_name
unique_id = str(uuid.uuid4())[:8]
tenant = Tenant.objects.create(
name=f"Test Business {unique_id}",
schema_name=f"testbiz{unique_id}"
)
user = User(
username=f"platformuser{unique_id}",
email=f"platform{unique_id}@example.com",
role=User.Role.PLATFORM_MANAGER,
tenant=tenant # Should be cleared
tenant=shared_tenant # Should be cleared
)
# Act
@@ -527,55 +507,43 @@ class TestSaveMethodValidation:
assert "must be assigned to a tenant" in str(exc_info.value)
@pytest.mark.django_db
def test_allows_tenant_user_with_tenant(self):
# Arrange
from smoothschedule.identity.core.models import Tenant
def test_allows_tenant_user_with_tenant(self, shared_tenant):
# Arrange - use shared fixture to avoid ~40s migration overhead
import uuid
unique_id = str(uuid.uuid4())[:8]
tenant = Tenant.objects.create(
name=f"Test Business {unique_id}",
schema_name=f"testbiz{unique_id}"
)
user = User(
username=f"owner{unique_id}",
email=f"owner{unique_id}@testbiz.com",
role=User.Role.TENANT_OWNER,
tenant=tenant
tenant=shared_tenant
)
# Act
user.save()
# Assert
assert user.tenant == tenant
assert user.tenant == shared_tenant
assert user.id is not None
@pytest.mark.django_db
def test_allows_customer_with_tenant(self):
# Arrange
from smoothschedule.identity.core.models import Tenant
def test_allows_customer_with_tenant(self, shared_tenant):
# Arrange - use shared fixture to avoid ~40s migration overhead
import uuid
unique_id = str(uuid.uuid4())[:8]
tenant = Tenant.objects.create(
name=f"Test Business {unique_id}",
schema_name=f"testbiz{unique_id}"
)
user = User(
username=f"customer{unique_id}",
email=f"customer{unique_id}@example.com",
role=User.Role.CUSTOMER,
tenant=tenant
tenant=shared_tenant
)
# Act
user.save()
# Assert
assert user.tenant == tenant
assert user.tenant == shared_tenant
# =============================================================================

View File

@@ -494,29 +494,18 @@ class TestGetAccessibleTenants:
assert result == mock_queryset
@pytest.mark.django_db
def test_returns_single_tenant_for_tenant_user(self):
# This test requires DB to create a real Tenant instance
# because Django's ForeignKey and ORM make mocking too complex
from smoothschedule.identity.core.models import Tenant
import uuid
# Create a real tenant
unique_id = str(uuid.uuid4())[:8]
tenant = Tenant.objects.create(
name=f'Test Business {unique_id}',
schema_name=f'testbiz{unique_id}'
)
def test_returns_single_tenant_for_tenant_user(self, shared_tenant):
# Use shared fixture to avoid ~40s migration overhead per tenant
# Create user with that tenant
user = create_user_instance(User.Role.TENANT_OWNER)
user.tenant = tenant
user.tenant = shared_tenant
# Act
result = user.get_accessible_tenants()
# Assert
assert result.count() == 1
assert list(result)[0] == tenant
assert list(result)[0] == shared_tenant
def test_returns_empty_queryset_for_tenant_user_without_tenant(self):
# Arrange
@@ -633,22 +622,16 @@ class TestSaveMethodValidation:
assert user.is_superuser is True
@pytest.mark.django_db
def test_clears_tenant_for_platform_users(self):
from smoothschedule.identity.core.models import Tenant
def test_clears_tenant_for_platform_users(self, shared_tenant):
# Use shared fixture to avoid ~40s migration overhead per tenant
import uuid
# Create unique schema name to avoid collisions
unique_id = str(uuid.uuid4())[:8]
tenant = Tenant.objects.create(
name=f'Test Business {unique_id}',
schema_name=f'testbiz{unique_id}'
)
user = User(
username='platformuser',
email='platform@example.com',
username=f'platformuser{unique_id}',
email=f'platform{unique_id}@example.com',
role=User.Role.PLATFORM_MANAGER,
tenant=tenant # Should be cleared
tenant=shared_tenant # Should be cleared
)
user.save()
@@ -669,49 +652,37 @@ class TestSaveMethodValidation:
assert 'must be assigned to a tenant' in str(exc_info.value)
@pytest.mark.django_db
def test_allows_tenant_user_with_tenant(self):
from smoothschedule.identity.core.models import Tenant
def test_allows_tenant_user_with_tenant(self, shared_tenant):
# Use shared fixture to avoid ~40s migration overhead per tenant
import uuid
# Create unique schema name to avoid collisions
unique_id = str(uuid.uuid4())[:8]
tenant = Tenant.objects.create(
name=f'Test Business {unique_id}',
schema_name=f'testbiz{unique_id}'
)
user = User(
username='owner',
email='owner@testbiz.com',
username=f'owner{unique_id}',
email=f'owner{unique_id}@testbiz.com',
role=User.Role.TENANT_OWNER,
tenant=tenant
tenant=shared_tenant
)
user.save()
assert user.tenant == tenant
assert user.tenant == shared_tenant
assert user.id is not None
@pytest.mark.django_db
def test_allows_customer_with_tenant(self):
from smoothschedule.identity.core.models import Tenant
def test_allows_customer_with_tenant(self, shared_tenant):
# Use shared fixture to avoid ~40s migration overhead per tenant
import uuid
# Create unique schema name to avoid collisions
unique_id = str(uuid.uuid4())[:8]
tenant = Tenant.objects.create(
name=f'Test Business {unique_id}',
schema_name=f'testbiz{unique_id}'
)
user = User(
username='customer',
email='customer@example.com',
username=f'customer{unique_id}',
email=f'customer{unique_id}@example.com',
role=User.Role.CUSTOMER,
tenant=tenant
tenant=shared_tenant
)
user.save()
assert user.tenant == tenant
assert user.tenant == shared_tenant
# =============================================================================