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:
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user