diff --git a/smoothschedule/CLAUDE.md b/smoothschedule/CLAUDE.md index ddb67d4..f3402f4 100644 --- a/smoothschedule/CLAUDE.md +++ b/smoothschedule/CLAUDE.md @@ -192,3 +192,222 @@ You're trying to run Python directly instead of through Docker. Use `docker comp ### Tenant (identity/core/models.py) - `name`, `subdomain`, `schema_name` - Multi-tenancy via django-tenants + +## Testing Guidelines + +### Testing Philosophy + +Follow the **Testing Pyramid**: +``` + /\ + /E2E\ <- Few (real browser, full stack) - AVOID + /------\ + /Integr- \ <- Some (real DB, verify ORM/queries) + /---ation--\ + / Unit \ <- Many (mocked, fast, isolated) - PREFER + /--------------\ +``` + +**Why:** Django-tenants requires full PostgreSQL migrations per test, making DB-based tests extremely slow (~30-60 seconds each). Unit tests with mocks run in milliseconds. + +### Unit Tests (Preferred) + +Write fast, isolated unit tests using mocks. **Do NOT use `@pytest.mark.django_db`** for unit tests. + +```python +# GOOD: Fast unit test with mocks +from unittest.mock import Mock, patch + +def test_event_service_creates_event(): + # Arrange - mock dependencies + mock_repo = Mock() + mock_repo.save.return_value = Mock(id=1, title="Test Event") + + mock_tenant = Mock() + mock_tenant.can_use_feature.return_value = True + + service = EventService(repository=mock_repo, tenant=mock_tenant) + + # Act + result = service.create_event(title="Test Event", start_time=datetime.now()) + + # Assert + mock_repo.save.assert_called_once() + assert result.title == "Test Event" + + +def test_event_service_denies_without_permission(): + mock_tenant = Mock() + mock_tenant.can_use_feature.return_value = False + + service = EventService(repository=Mock(), tenant=mock_tenant) + + with pytest.raises(PermissionDenied): + service.create_event(title="Test") +``` + +```python +# BAD: Slow test hitting real database +@pytest.mark.django_db +def test_event_creation(tenant, user): + # This takes 30+ seconds due to tenant schema setup! + event = Event.objects.create(tenant=tenant, title="Test") + assert event.id is not None +``` + +### Testing Serializers + +```python +def test_event_serializer_validates_end_after_start(): + data = { + 'title': 'Test', + 'start_time': '2024-01-01T10:00:00Z', + 'end_time': '2024-01-01T09:00:00Z', # Before start! + } + serializer = EventSerializer(data=data) + + assert not serializer.is_valid() + assert 'end_time' in serializer.errors +``` + +### Testing Views/ViewSets + +Use `APIRequestFactory` with mocked authentication: + +```python +from rest_framework.test import APIRequestFactory +from unittest.mock import Mock, patch + +def test_resource_list_returns_filtered_resources(): + factory = APIRequestFactory() + request = factory.get('/api/resources/') + + # Mock the user and tenant + request.user = Mock(is_authenticated=True, role='manager') + request.tenant = Mock(id=1) + + # Mock the queryset + with patch.object(ResourceViewSet, 'get_queryset') as mock_qs: + mock_qs.return_value = [Mock(id=1, name='Room A')] + + view = ResourceViewSet.as_view({'get': 'list'}) + response = view(request) + + assert response.status_code == 200 +``` + +### Integration Tests (Use Sparingly) + +Only use `@pytest.mark.django_db` when you MUST verify: +- Complex ORM queries with joins/aggregations +- Database constraints and triggers +- Migration correctness + +```python +# Integration test - use only when necessary +@pytest.mark.django_db +class TestEventQueryIntegration: + """Verify complex queries work correctly with real DB.""" + + def test_overlapping_events_query(self, tenant_with_events): + # This tests the actual SQL query behavior + overlapping = Event.objects.filter_overlapping( + start=datetime(2024, 1, 1, 10), + end=datetime(2024, 1, 1, 11) + ) + assert overlapping.count() == 2 +``` + +### Writing Testable Code + +Design code for testability using dependency injection: + +```python +# BAD: Hard to test - direct imports and global state +class EventService: + def create_event(self, data): + tenant = get_current_tenant() # Global state! + event = Event.objects.create(**data) # Direct ORM call + send_notification(event) # Direct import + return event + +# GOOD: Easy to test - dependencies injected +class EventService: + def __init__(self, repository, tenant, notifier): + self.repository = repository + self.tenant = tenant + self.notifier = notifier + + def create_event(self, data): + if not self.tenant.can_use_feature('events'): + raise PermissionDenied() + event = self.repository.save(Event(**data)) + self.notifier.send(event) + return event +``` + +### Repository Pattern + +Abstract database access for easier mocking: + +```python +# In repositories.py +class EventRepository: + def save(self, event): + event.save() + return event + + def get_by_id(self, event_id): + return Event.objects.get(id=event_id) + + def filter_by_date_range(self, start, end): + return Event.objects.filter(start_time__gte=start, end_time__lte=end) + +# In tests - mock the repository +def test_something(): + mock_repo = Mock(spec=EventRepository) + mock_repo.get_by_id.return_value = Mock(id=1, title="Test") + # ... test with mock_repo +``` + +### Test File Structure + +Each app should have: +``` +app/ +├── tests/ +│ ├── __init__.py +│ ├── test_services.py # Unit tests for business logic +│ ├── test_serializers.py # Serializer validation tests +│ ├── test_views.py # View/ViewSet tests (mocked) +│ └── test_integration.py # DB tests (sparingly) +``` + +### Running Tests + +```bash +# Run all tests +docker compose -f docker-compose.local.yml exec django pytest + +# Run specific test file +docker compose -f docker-compose.local.yml exec django pytest smoothschedule/scheduling/schedule/tests/test_services.py + +# Run with coverage +docker compose -f docker-compose.local.yml exec django coverage run -m pytest +docker compose -f docker-compose.local.yml exec django coverage report + +# Run only unit tests (fast) +docker compose -f docker-compose.local.yml exec django pytest -m "not django_db" + +# Run only integration tests +docker compose -f docker-compose.local.yml exec django pytest -m django_db +``` + +### Key Testing Rules + +1. **Default to mocks** - Only hit DB when testing query logic +2. **One assertion focus** - Each test verifies one behavior +3. **Descriptive names** - `test_create_event_raises_when_tenant_lacks_permission` +4. **Arrange-Act-Assert** - Clear test structure +5. **No test interdependence** - Tests must run in any order +6. **Fast feedback** - If a test takes >1 second, it probably shouldn't hit the DB