--- name: lp-testing description: Creates tests for league-planner using Django TestCase with multi-database support, CRUD testing tags, middleware modification, and API testing patterns. Use for writing tests. argument-hint: allowed-tools: Read, Write, Edit, Glob, Grep --- # League-Planner Testing Guide Creates comprehensive tests following league-planner patterns: Django TestCase with multi-database support, tagged test organization, middleware modification for isolation, API testing with DRF, and factory patterns. ## When to Use - Writing unit tests for models and business logic - Creating integration tests for views and APIs - Testing Celery tasks - Implementing CRUD operation tests - Setting up test fixtures and factories ## Test Structure ``` common/tests/ ├── test_crud.py # CRUD operations (tag: crud) ├── test_gui.py # GUI/Template tests ├── test_selenium.py # Browser automation ├── __init__.py api/tests/ ├── tests.py # API endpoint tests ├── __init__.py scheduler/ ├── tests.py # Model and helper tests ├── __init__.py {app}/tests/ ├── test_models.py # Model tests ├── test_views.py # View tests ├── test_api.py # API tests ├── test_tasks.py # Celery task tests ├── factories.py # Test factories └── __init__.py ``` ## Instructions ### Step 1: Create Test Base Class ```python # common/tests/base.py from django.test import TestCase, TransactionTestCase, override_settings, modify_settings from django.contrib.auth import get_user_model from rest_framework.test import APITestCase, APIClient User = get_user_model() class BaseTestCase(TestCase): """Base test case with common setup.""" # Use all databases for multi-DB support databases = '__all__' @classmethod def setUpTestData(cls): """Set up data for the whole test class.""" # Create test user cls.user = User.objects.create_user( username='testuser', email='test@example.com', password='testpass123', ) cls.admin_user = User.objects.create_superuser( username='admin', email='admin@example.com', password='adminpass123', ) def setUp(self): """Set up for each test method.""" self.client.login(username='testuser', password='testpass123') class BaseAPITestCase(APITestCase): """Base API test case.""" databases = '__all__' @classmethod def setUpTestData(cls): cls.user = User.objects.create_user( username='apiuser', email='api@example.com', password='apipass123', ) cls.admin_user = User.objects.create_superuser( username='apiadmin', email='apiadmin@example.com', password='adminpass123', ) def setUp(self): self.client = APIClient() def authenticate_user(self): """Authenticate as regular user.""" self.client.force_authenticate(user=self.user) def authenticate_admin(self): """Authenticate as admin.""" self.client.force_authenticate(user=self.admin_user) @modify_settings(MIDDLEWARE={ 'remove': [ 'common.middleware.LoginRequiredMiddleware', 'common.middleware.AdminMiddleware', 'common.middleware.URLMiddleware', 'common.middleware.MenuMiddleware', 'common.middleware.ErrorHandlerMiddleware', ] }) class IsolatedTestCase(BaseTestCase): """Test case with middleware removed for isolation.""" pass ``` ### Step 2: Create Test Factories ```python # scheduler/tests/factories.py import factory from factory.django import DjangoModelFactory from django.contrib.auth import get_user_model from scheduler.models import League, Season, Team, Scenario, Match, Day User = get_user_model() class UserFactory(DjangoModelFactory): class Meta: model = User username = factory.Sequence(lambda n: f'user{n}') email = factory.LazyAttribute(lambda obj: f'{obj.username}@example.com') password = factory.PostGenerationMethodCall('set_password', 'testpass123') class LeagueFactory(DjangoModelFactory): class Meta: model = League name = factory.Sequence(lambda n: f'Test League {n}') abbreviation = factory.Sequence(lambda n: f'TL{n}') sport = 'football' country = 'DE' class SeasonFactory(DjangoModelFactory): class Meta: model = Season league = factory.SubFactory(LeagueFactory) name = factory.Sequence(lambda n: f'Season {n}') start_date = factory.Faker('date_this_year') end_date = factory.Faker('date_this_year') num_teams = 18 num_rounds = 34 class TeamFactory(DjangoModelFactory): class Meta: model = Team season = factory.SubFactory(SeasonFactory) name = factory.Sequence(lambda n: f'Team {n}') abbreviation = factory.Sequence(lambda n: f'T{n:02d}') city = factory.Faker('city') latitude = factory.Faker('latitude') longitude = factory.Faker('longitude') class ScenarioFactory(DjangoModelFactory): class Meta: model = Scenario season = factory.SubFactory(SeasonFactory) name = factory.Sequence(lambda n: f'Scenario {n}') is_active = True class DayFactory(DjangoModelFactory): class Meta: model = Day scenario = factory.SubFactory(ScenarioFactory) number = factory.Sequence(lambda n: n + 1) date = factory.Faker('date_this_year') class MatchFactory(DjangoModelFactory): class Meta: model = Match scenario = factory.SubFactory(ScenarioFactory) home_team = factory.SubFactory(TeamFactory) away_team = factory.SubFactory(TeamFactory) day = factory.SubFactory(DayFactory) ``` ### Step 3: Write Model Tests ```python # scheduler/tests/test_models.py from django.test import TestCase, tag from django.db import IntegrityError from django.core.exceptions import ValidationError from scheduler.models import League, Season, Team, Scenario, Match from .factories import ( LeagueFactory, SeasonFactory, TeamFactory, ScenarioFactory, MatchFactory, DayFactory, ) class LeagueModelTest(TestCase): """Tests for League model.""" databases = '__all__' def test_create_league(self): """Test basic league creation.""" league = LeagueFactory() self.assertIsNotNone(league.pk) self.assertTrue(league.name.startswith('Test League')) def test_league_str(self): """Test string representation.""" league = LeagueFactory(name='Bundesliga') self.assertEqual(str(league), 'Bundesliga') def test_league_managers_relation(self): """Test managers M2M relationship.""" from django.contrib.auth import get_user_model User = get_user_model() league = LeagueFactory() user = User.objects.create_user('manager', 'manager@test.com', 'pass') league.managers.add(user) self.assertIn(user, league.managers.all()) class SeasonModelTest(TestCase): """Tests for Season model.""" databases = '__all__' def test_create_season(self): """Test season creation with league.""" season = SeasonFactory() self.assertIsNotNone(season.pk) self.assertIsNotNone(season.league) def test_season_team_count(self): """Test team counting on season.""" season = SeasonFactory() TeamFactory.create_batch(5, season=season) self.assertEqual(season.teams.count(), 5) class ScenarioModelTest(TestCase): """Tests for Scenario model.""" databases = '__all__' def test_create_scenario(self): """Test scenario creation.""" scenario = ScenarioFactory() self.assertIsNotNone(scenario.pk) self.assertTrue(scenario.is_active) @tag('slow') def test_scenario_copy(self): """Test deep copy of scenario.""" from scheduler.helpers import copy_scenario # Create scenario with matches scenario = ScenarioFactory() day = DayFactory(scenario=scenario) home = TeamFactory(season=scenario.season) away = TeamFactory(season=scenario.season) MatchFactory(scenario=scenario, home_team=home, away_team=away, day=day) # Copy scenario new_scenario = copy_scenario(scenario, suffix='_copy') self.assertNotEqual(scenario.pk, new_scenario.pk) self.assertEqual(new_scenario.matches.count(), 1) self.assertIn('_copy', new_scenario.name) class MatchModelTest(TestCase): """Tests for Match model.""" databases = '__all__' def test_create_match(self): """Test match creation.""" match = MatchFactory() self.assertIsNotNone(match.pk) self.assertNotEqual(match.home_team, match.away_team) def test_match_constraint_different_teams(self): """Test that home and away team must be different.""" scenario = ScenarioFactory() team = TeamFactory(season=scenario.season) with self.assertRaises(IntegrityError): Match.objects.create( scenario=scenario, home_team=team, away_team=team, # Same team! ) def test_match_optimized_query(self): """Test optimized query method.""" scenario = ScenarioFactory() MatchFactory.create_batch(10, scenario=scenario) with self.assertNumQueries(1): matches = list(Match.get_for_scenario(scenario.pk)) # Access related fields for m in matches: _ = m.home_team.name _ = m.away_team.name ``` ### Step 4: Write View Tests ```python # scheduler/tests/test_views.py from django.test import TestCase, Client, tag, override_settings, modify_settings from django.urls import reverse from django.contrib.auth import get_user_model from scheduler.models import League, Season, Scenario from .factories import LeagueFactory, SeasonFactory, ScenarioFactory, UserFactory User = get_user_model() @tag('crud') @modify_settings(MIDDLEWARE={ 'remove': [ 'common.middleware.LoginRequiredMiddleware', 'common.middleware.URLMiddleware', 'common.middleware.MenuMiddleware', ] }) class CRUDViewsTest(TestCase): """Test CRUD views for scheduler models.""" databases = '__all__' @classmethod def setUpTestData(cls): cls.user = User.objects.create_superuser( username='admin', email='admin@test.com', password='adminpass', ) def setUp(self): self.client = Client() self.client.login(username='admin', password='adminpass') def test_league_create(self): """Test league creation via view.""" url = reverse('league-add') data = { 'name': 'New Test League', 'abbreviation': 'NTL', 'sport': 'football', } response = self.client.post(url, data) self.assertEqual(response.status_code, 302) # Redirect on success self.assertTrue(League.objects.filter(name='New Test League').exists()) def test_league_update(self): """Test league update via view.""" league = LeagueFactory() url = reverse('league-edit', kwargs={'pk': league.pk}) data = { 'name': 'Updated League Name', 'abbreviation': league.abbreviation, 'sport': league.sport, } response = self.client.post(url, data) self.assertEqual(response.status_code, 302) league.refresh_from_db() self.assertEqual(league.name, 'Updated League Name') def test_league_delete(self): """Test league deletion via view.""" league = LeagueFactory() url = reverse('league-delete', kwargs={'pk': league.pk}) response = self.client.post(url) self.assertEqual(response.status_code, 302) self.assertFalse(League.objects.filter(pk=league.pk).exists()) def test_scenario_list_view(self): """Test scenario listing view.""" season = SeasonFactory() ScenarioFactory.create_batch(3, season=season) # Set session session = self.client.session session['league'] = season.league.pk session['season'] = season.pk session.save() url = reverse('scenarios') response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.context['scenarios']), 3) class PermissionViewsTest(TestCase): """Test view permission checks.""" databases = '__all__' def setUp(self): self.client = Client() self.regular_user = User.objects.create_user( username='regular', email='regular@test.com', password='pass123', ) self.staff_user = User.objects.create_user( username='staff', email='staff@test.com', password='pass123', is_staff=True, ) self.league = LeagueFactory() self.league.managers.add(self.staff_user) def test_unauthorized_access_redirects(self): """Test that unauthorized users are redirected.""" url = reverse('league-edit', kwargs={'pk': self.league.pk}) self.client.login(username='regular', password='pass123') response = self.client.get(url) self.assertIn(response.status_code, [302, 403]) def test_manager_can_access(self): """Test that league managers can access.""" url = reverse('league-edit', kwargs={'pk': self.league.pk}) self.client.login(username='staff', password='pass123') # Set session session = self.client.session session['league'] = self.league.pk session.save() response = self.client.get(url) self.assertEqual(response.status_code, 200) ``` ### Step 5: Write API Tests ```python # api/tests/tests.py from django.test import tag from rest_framework.test import APITestCase, APIClient from rest_framework import status from django.contrib.auth import get_user_model from scheduler.models import Team, Season from scheduler.tests.factories import ( SeasonFactory, TeamFactory, ScenarioFactory, ) User = get_user_model() class TeamsAPITest(APITestCase): """Test Teams API endpoints.""" databases = '__all__' @classmethod def setUpTestData(cls): cls.user = User.objects.create_user( username='apiuser', email='api@test.com', password='apipass123', ) cls.season = SeasonFactory() cls.teams = TeamFactory.create_batch(5, season=cls.season) def setUp(self): self.client = APIClient() def test_list_teams_unauthenticated(self): """Test listing teams without authentication.""" url = f'/api/uefa/v2/teams/?season_id={self.season.pk}' response = self.client.get(url) # Public endpoint should work self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data), 5) def test_list_teams_with_filter(self): """Test filtering teams by active status.""" # Deactivate some teams Team.objects.filter(pk__in=[self.teams[0].pk, self.teams[1].pk]).update(is_active=False) url = f'/api/uefa/v2/teams/?season_id={self.season.pk}&active_only=true' response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data), 3) def test_list_teams_missing_season(self): """Test error when season_id is missing.""" url = '/api/uefa/v2/teams/' response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertIn('error', response.data) def test_team_token_authentication(self): """Test authentication with team token.""" team = self.teams[0] team.hashval = 'test-token-12345' team.save() url = f'/api/uefa/v2/team/{team.pk}/schedule/' self.client.credentials(HTTP_X_TEAM_TOKEN='test-token-12345') response = self.client.get(url) self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_404_NOT_FOUND]) @tag('api') class DrawsAPITest(APITestCase): """Test Draws API endpoints.""" databases = '__all__' @classmethod def setUpTestData(cls): cls.user = User.objects.create_superuser( username='admin', email='admin@test.com', password='adminpass', ) cls.season = SeasonFactory() def setUp(self): self.client = APIClient() self.client.force_authenticate(user=self.user) def test_create_draw(self): """Test creating a new draw.""" url = '/api/uefa/v2/draws/create/' data = { 'season_id': self.season.pk, 'name': 'Test Draw', 'mode': 'groups', } response = self.client.post(url, data, format='json') self.assertIn(response.status_code, [status.HTTP_201_CREATED, status.HTTP_200_OK]) ``` ### Step 6: Write Celery Task Tests ```python # scheduler/tests/test_tasks.py from django.test import TestCase, TransactionTestCase, override_settings, tag from unittest.mock import patch, MagicMock from celery.contrib.testing.worker import start_worker from scheduler.models import Scenario from .factories import ScenarioFactory, TeamFactory, MatchFactory, DayFactory @tag('celery', 'slow') class CeleryTaskTest(TransactionTestCase): """Test Celery tasks.""" databases = '__all__' def test_optimization_task_sync(self): """Test optimization task in synchronous mode.""" from scheduler.solver.tasks import task_optimize_scenario scenario = ScenarioFactory() home = TeamFactory(season=scenario.season) away = TeamFactory(season=scenario.season) day = DayFactory(scenario=scenario) MatchFactory(scenario=scenario, home_team=home, away_team=away, day=day) # Run task synchronously result = task_optimize_scenario( scenario_id=scenario.pk, options={'time_limit': 10}, ) self.assertIn(result['status'], ['optimal', 'feasible', 'timeout', 'error']) @patch('scheduler.solver.optimizer.PuLPOptimizer') def test_optimization_task_mocked(self, MockOptimizer): """Test optimization task with mocked solver.""" from scheduler.solver.tasks import task_optimize_scenario from scheduler.solver.optimizer import OptimizationResult # Setup mock mock_instance = MockOptimizer.return_value mock_instance.solve.return_value = OptimizationResult( status='optimal', objective_value=100.0, solution={'x': {(1, 1, 1): 1.0}}, solve_time=1.5, gap=0.0, iterations=100, ) scenario = ScenarioFactory() result = task_optimize_scenario(scenario_id=scenario.pk) self.assertEqual(result['status'], 'optimal') mock_instance.build_model.assert_called_once() mock_instance.solve.assert_called_once() def test_task_abort(self): """Test task abort handling.""" from scheduler.solver.tasks import task_optimize_scenario from unittest.mock import PropertyMock scenario = ScenarioFactory() # Create mock task with is_aborted returning True with patch.object(task_optimize_scenario, 'is_aborted', return_value=True): # This would require more setup to properly test pass class TaskProgressTest(TestCase): """Test task progress tracking.""" databases = '__all__' def test_task_record_creation(self): """Test TaskRecord is created on task start.""" from taskmanager.models import Task as TaskRecord initial_count = TaskRecord.objects.count() # Simulate task creating record record = TaskRecord.objects.create( task_id='test-task-id', task_name='test.task', scenario_id=1, ) self.assertEqual(TaskRecord.objects.count(), initial_count + 1) self.assertEqual(record.progress, 0) def test_task_progress_update(self): """Test progress update.""" from taskmanager.models import Task as TaskRecord record = TaskRecord.objects.create( task_id='test-task-id', task_name='test.task', ) record.update_progress(50, 'Halfway done') record.refresh_from_db() self.assertEqual(record.progress, 50) self.assertEqual(record.status_message, 'Halfway done') ``` ## Running Tests ```bash # Run all tests python manage.py test # Run with tag python manage.py test --tag crud python manage.py test --tag api python manage.py test --exclude-tag slow # Run specific app tests python manage.py test api.tests python manage.py test scheduler.tests # Run specific test file python manage.py test common.tests.test_crud # Run specific test class python manage.py test scheduler.tests.test_models.MatchModelTest # Run specific test method python manage.py test scheduler.tests.test_models.MatchModelTest.test_create_match # With verbosity python manage.py test -v 2 # With coverage coverage run manage.py test coverage report coverage html ``` ## Common Pitfalls - **Missing `databases = '__all__'`**: Required for multi-database tests - **Middleware interference**: Use `@modify_settings` to remove problematic middleware - **Session not set**: Set session values before testing session-dependent views - **Factory relationships**: Ensure factory SubFactory references match your model structure - **Transaction issues**: Use `TransactionTestCase` for tests requiring actual commits ## Verification After writing tests: ```bash # Check test discovery python manage.py test --list # Run with failfast python manage.py test --failfast # Check coverage coverage run --source='scheduler,api,common' manage.py test coverage report --fail-under=80 ```