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==9.0.1",
|
||||||
"pytest-django==4.11.1",
|
"pytest-django==4.11.1",
|
||||||
"pytest-sugar==1.1.1",
|
"pytest-sugar==1.1.1",
|
||||||
|
"pytest-xdist>=3.5.0",
|
||||||
"ruff==0.14.6",
|
"ruff==0.14.6",
|
||||||
"sphinx==8.2.3",
|
"sphinx==8.2.3",
|
||||||
"sphinx-autobuild==2025.8.25",
|
"sphinx-autobuild==2025.8.25",
|
||||||
|
|||||||
@@ -18,3 +18,56 @@ def user(db) -> User:
|
|||||||
For unit tests, use create_mock_user() from factories.py instead.
|
For unit tests, use create_mock_user() from factories.py instead.
|
||||||
"""
|
"""
|
||||||
return UserFactory()
|
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.
|
Test get_accessible_tenants() method.
|
||||||
|
|
||||||
Note: These tests use database access because the method accesses
|
Uses shared tenant fixtures (session-scoped) to avoid ~40s migration overhead
|
||||||
ForeignKey relationships which trigger database queries even with mocking.
|
per tenant creation. See conftest.py for fixture definitions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_returns_all_tenants_for_platform_user(self):
|
def test_returns_all_tenants_for_platform_user(self, shared_tenant, second_shared_tenant):
|
||||||
# Arrange
|
# Arrange - use shared fixtures instead of creating new tenants
|
||||||
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}")
|
|
||||||
|
|
||||||
user = create_user_instance(User.Role.PLATFORM_MANAGER)
|
user = create_user_instance(User.Role.PLATFORM_MANAGER)
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
result = user.get_accessible_tenants()
|
result = user.get_accessible_tenants()
|
||||||
|
|
||||||
# Assert
|
# Assert - at least our two shared tenants exist
|
||||||
assert result.count() >= 2 # At least the two we created
|
assert result.count() >= 2
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_returns_single_tenant_for_tenant_user(self):
|
def test_returns_single_tenant_for_tenant_user(self, shared_tenant):
|
||||||
# Arrange
|
# Arrange - use shared fixture
|
||||||
from smoothschedule.identity.core.models import Tenant
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
unique_id = str(uuid.uuid4())[:8]
|
unique_id = str(uuid.uuid4())[:8]
|
||||||
tenant = Tenant.objects.create(name=f"My Business {unique_id}", schema_name=f"mybiz{unique_id}")
|
|
||||||
|
|
||||||
user = User(
|
user = User(
|
||||||
username=f"owner{unique_id}",
|
username=f"owner{unique_id}",
|
||||||
email=f"owner{unique_id}@test.com",
|
email=f"owner{unique_id}@test.com",
|
||||||
role=User.Role.TENANT_OWNER,
|
role=User.Role.TENANT_OWNER,
|
||||||
tenant=tenant
|
tenant=shared_tenant
|
||||||
)
|
)
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
@@ -388,7 +375,7 @@ class TestGetAccessibleTenants:
|
|||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert result.count() == 1
|
assert result.count() == 1
|
||||||
assert result.first() == tenant
|
assert result.first() == shared_tenant
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_returns_empty_queryset_for_tenant_user_without_tenant(self):
|
def test_returns_empty_queryset_for_tenant_user_without_tenant(self):
|
||||||
@@ -482,23 +469,16 @@ class TestSaveMethodValidation:
|
|||||||
assert user.is_superuser is True
|
assert user.is_superuser is True
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_clears_tenant_for_platform_users(self):
|
def test_clears_tenant_for_platform_users(self, shared_tenant):
|
||||||
# Arrange
|
# Arrange - use shared fixture to avoid ~40s migration overhead
|
||||||
from smoothschedule.identity.core.models import Tenant
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
# Create a tenant first with unique schema_name
|
|
||||||
unique_id = str(uuid.uuid4())[:8]
|
unique_id = str(uuid.uuid4())[:8]
|
||||||
tenant = Tenant.objects.create(
|
|
||||||
name=f"Test Business {unique_id}",
|
|
||||||
schema_name=f"testbiz{unique_id}"
|
|
||||||
)
|
|
||||||
|
|
||||||
user = User(
|
user = User(
|
||||||
username=f"platformuser{unique_id}",
|
username=f"platformuser{unique_id}",
|
||||||
email=f"platform{unique_id}@example.com",
|
email=f"platform{unique_id}@example.com",
|
||||||
role=User.Role.PLATFORM_MANAGER,
|
role=User.Role.PLATFORM_MANAGER,
|
||||||
tenant=tenant # Should be cleared
|
tenant=shared_tenant # Should be cleared
|
||||||
)
|
)
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
@@ -527,55 +507,43 @@ class TestSaveMethodValidation:
|
|||||||
assert "must be assigned to a tenant" in str(exc_info.value)
|
assert "must be assigned to a tenant" in str(exc_info.value)
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_allows_tenant_user_with_tenant(self):
|
def test_allows_tenant_user_with_tenant(self, shared_tenant):
|
||||||
# Arrange
|
# Arrange - use shared fixture to avoid ~40s migration overhead
|
||||||
from smoothschedule.identity.core.models import Tenant
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
unique_id = str(uuid.uuid4())[:8]
|
unique_id = str(uuid.uuid4())[:8]
|
||||||
tenant = Tenant.objects.create(
|
|
||||||
name=f"Test Business {unique_id}",
|
|
||||||
schema_name=f"testbiz{unique_id}"
|
|
||||||
)
|
|
||||||
|
|
||||||
user = User(
|
user = User(
|
||||||
username=f"owner{unique_id}",
|
username=f"owner{unique_id}",
|
||||||
email=f"owner{unique_id}@testbiz.com",
|
email=f"owner{unique_id}@testbiz.com",
|
||||||
role=User.Role.TENANT_OWNER,
|
role=User.Role.TENANT_OWNER,
|
||||||
tenant=tenant
|
tenant=shared_tenant
|
||||||
)
|
)
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert user.tenant == tenant
|
assert user.tenant == shared_tenant
|
||||||
assert user.id is not None
|
assert user.id is not None
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_allows_customer_with_tenant(self):
|
def test_allows_customer_with_tenant(self, shared_tenant):
|
||||||
# Arrange
|
# Arrange - use shared fixture to avoid ~40s migration overhead
|
||||||
from smoothschedule.identity.core.models import Tenant
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
unique_id = str(uuid.uuid4())[:8]
|
unique_id = str(uuid.uuid4())[:8]
|
||||||
tenant = Tenant.objects.create(
|
|
||||||
name=f"Test Business {unique_id}",
|
|
||||||
schema_name=f"testbiz{unique_id}"
|
|
||||||
)
|
|
||||||
|
|
||||||
user = User(
|
user = User(
|
||||||
username=f"customer{unique_id}",
|
username=f"customer{unique_id}",
|
||||||
email=f"customer{unique_id}@example.com",
|
email=f"customer{unique_id}@example.com",
|
||||||
role=User.Role.CUSTOMER,
|
role=User.Role.CUSTOMER,
|
||||||
tenant=tenant
|
tenant=shared_tenant
|
||||||
)
|
)
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert user.tenant == tenant
|
assert user.tenant == shared_tenant
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
@@ -494,29 +494,18 @@ class TestGetAccessibleTenants:
|
|||||||
assert result == mock_queryset
|
assert result == mock_queryset
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_returns_single_tenant_for_tenant_user(self):
|
def test_returns_single_tenant_for_tenant_user(self, shared_tenant):
|
||||||
# This test requires DB to create a real Tenant instance
|
# Use shared fixture to avoid ~40s migration overhead per tenant
|
||||||
# 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}'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create user with that tenant
|
# Create user with that tenant
|
||||||
user = create_user_instance(User.Role.TENANT_OWNER)
|
user = create_user_instance(User.Role.TENANT_OWNER)
|
||||||
user.tenant = tenant
|
user.tenant = shared_tenant
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
result = user.get_accessible_tenants()
|
result = user.get_accessible_tenants()
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert result.count() == 1
|
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):
|
def test_returns_empty_queryset_for_tenant_user_without_tenant(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
@@ -633,22 +622,16 @@ class TestSaveMethodValidation:
|
|||||||
assert user.is_superuser is True
|
assert user.is_superuser is True
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_clears_tenant_for_platform_users(self):
|
def test_clears_tenant_for_platform_users(self, shared_tenant):
|
||||||
from smoothschedule.identity.core.models import Tenant
|
# Use shared fixture to avoid ~40s migration overhead per tenant
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
# Create unique schema name to avoid collisions
|
|
||||||
unique_id = str(uuid.uuid4())[:8]
|
unique_id = str(uuid.uuid4())[:8]
|
||||||
tenant = Tenant.objects.create(
|
|
||||||
name=f'Test Business {unique_id}',
|
|
||||||
schema_name=f'testbiz{unique_id}'
|
|
||||||
)
|
|
||||||
|
|
||||||
user = User(
|
user = User(
|
||||||
username='platformuser',
|
username=f'platformuser{unique_id}',
|
||||||
email='platform@example.com',
|
email=f'platform{unique_id}@example.com',
|
||||||
role=User.Role.PLATFORM_MANAGER,
|
role=User.Role.PLATFORM_MANAGER,
|
||||||
tenant=tenant # Should be cleared
|
tenant=shared_tenant # Should be cleared
|
||||||
)
|
)
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
@@ -669,49 +652,37 @@ class TestSaveMethodValidation:
|
|||||||
assert 'must be assigned to a tenant' in str(exc_info.value)
|
assert 'must be assigned to a tenant' in str(exc_info.value)
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_allows_tenant_user_with_tenant(self):
|
def test_allows_tenant_user_with_tenant(self, shared_tenant):
|
||||||
from smoothschedule.identity.core.models import Tenant
|
# Use shared fixture to avoid ~40s migration overhead per tenant
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
# Create unique schema name to avoid collisions
|
|
||||||
unique_id = str(uuid.uuid4())[:8]
|
unique_id = str(uuid.uuid4())[:8]
|
||||||
tenant = Tenant.objects.create(
|
|
||||||
name=f'Test Business {unique_id}',
|
|
||||||
schema_name=f'testbiz{unique_id}'
|
|
||||||
)
|
|
||||||
|
|
||||||
user = User(
|
user = User(
|
||||||
username='owner',
|
username=f'owner{unique_id}',
|
||||||
email='owner@testbiz.com',
|
email=f'owner{unique_id}@testbiz.com',
|
||||||
role=User.Role.TENANT_OWNER,
|
role=User.Role.TENANT_OWNER,
|
||||||
tenant=tenant
|
tenant=shared_tenant
|
||||||
)
|
)
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
assert user.tenant == tenant
|
assert user.tenant == shared_tenant
|
||||||
assert user.id is not None
|
assert user.id is not None
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_allows_customer_with_tenant(self):
|
def test_allows_customer_with_tenant(self, shared_tenant):
|
||||||
from smoothschedule.identity.core.models import Tenant
|
# Use shared fixture to avoid ~40s migration overhead per tenant
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
# Create unique schema name to avoid collisions
|
|
||||||
unique_id = str(uuid.uuid4())[:8]
|
unique_id = str(uuid.uuid4())[:8]
|
||||||
tenant = Tenant.objects.create(
|
|
||||||
name=f'Test Business {unique_id}',
|
|
||||||
schema_name=f'testbiz{unique_id}'
|
|
||||||
)
|
|
||||||
|
|
||||||
user = User(
|
user = User(
|
||||||
username='customer',
|
username=f'customer{unique_id}',
|
||||||
email='customer@example.com',
|
email=f'customer{unique_id}@example.com',
|
||||||
role=User.Role.CUSTOMER,
|
role=User.Role.CUSTOMER,
|
||||||
tenant=tenant
|
tenant=shared_tenant
|
||||||
)
|
)
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
assert user.tenant == tenant
|
assert user.tenant == shared_tenant
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user