22 KiB
22 KiB
| name | description | argument-hint | allowed-tools |
|---|---|---|---|
| lp-testing | 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. | <test-type> <app-or-model> | 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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_settingsto 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
TransactionTestCasefor tests requiring actual commits
Verification
After writing tests:
# 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