2026-02-04 15:49:25 +01:00

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_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:

# 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