Skip to main content
FootyCollect uses pytest for testing with comprehensive coverage reporting. Tests are organized by app and functionality.

Quick Start

pytest

Test Configuration

pytest.ini

Main pytest configuration:
pytest.ini
[pytest]
DJANGO_SETTINGS_MODULE = config.settings.test
python_files = tests.py test_*.py *_tests.py
python_classes = Test* *Test *Tests
python_functions = test_*
addopts =
    --tb=short
    --strict-markers
    --disable-warnings
    --reuse-db
    --nomigrations
    --cov=footycollect
    --cov-report=html
    --cov-report=term-missing
testpaths =
    footycollect
markers =
    slow: marks tests as slow (deselect with '-m "not slow"')
    integration: marks tests as integration tests
    unit: marks tests as unit tests
    api: marks tests as API tests
    models: marks tests as model tests
    views: marks tests as view tests
    forms: marks tests as form tests
    client: marks tests as client tests
The --reuse-db flag reuses the test database between runs for faster execution. Use --create-db to recreate it.

pyproject.toml Configuration

pyproject.toml
[tool.pytest.ini_options]
minversion = "6.0"
addopts = "--ds=config.settings.test --reuse-db --import-mode=importlib"
python_files = [
    "tests.py",
    "test_*.py",
]

[tool.coverage.run]
include = ["footycollect/**"]
omit = ["*/migrations/*", "*/tests/*"]
plugins = ["django_coverage_plugin"]

Test Organization

Tests are organized by app and functionality:

Collection App Tests

footycollect/collection/tests/
├── test_models.py              # Model tests (BaseItem, Jersey, Photo)
├── test_views/                 # View tests organized by view type
│   ├── test_jersey_views.py
│   ├── test_item_views.py
│   ├── test_photo_views.py
│   └── test_feed_views.py
├── test_services.py            # Service layer tests
├── test_item_service.py        # Item service tests
├── test_photo_service.py       # Photo service tests
├── test_forms.py               # Form validation tests
├── test_repositories.py        # Repository layer tests
├── test_tasks.py               # Celery task tests
└── test_e2e_*.py              # End-to-end tests

Test Types

Test Django models:
  • Model creation and validation
  • Field constraints
  • Model methods
  • Relationships
def test_create_jersey(user, club, season):
    jersey = Jersey.objects.create(
        user=user,
        club=club,
        season=season,
        item_type="home",
    )
    assert jersey.club == club
    assert jersey.season == season
Test Django views:
  • URL resolution
  • View responses
  • Form handling
  • Permission checks
def test_jersey_list_view(client, user):
    client.force_login(user)
    response = client.get(reverse("collection:jersey-list"))
    assert response.status_code == 200
Test business logic in the service layer:
  • Service methods
  • Business rules
  • Data transformations
  • Error handling
def test_create_item_service(user, club, season):
    service = ItemService(user)
    item = service.create_jersey(
        club=club,
        season=season,
        item_type="home",
    )
    assert item.user == user
Test form validation:
  • Field validation
  • Form cleaning
  • Custom validators
  • Error messages
def test_jersey_form_valid_data():
    form = JerseyForm(data={"item_type": "home"})
    assert form.is_valid()

Running Tests by Marker

Use pytest markers to run specific test categories:
pytest -m unit

Coverage Reporting

FootyCollect aims for high test coverage:
1

Run tests with coverage

pytest
Coverage is automatically collected (configured in pytest.ini)
2

View HTML coverage report

open htmlcov/index.html
# On Linux: xdg-open htmlcov/index.html
# On Windows: start htmlcov/index.html
3

Check coverage percentage

Look for the coverage summary in the terminal output:
----------- coverage: platform linux, python 3.12 -----------
Name                                    Stmts   Miss  Cover   Missing
---------------------------------------------------------------------
footycollect/collection/models.py         234      5    98%   45-47
footycollect/collection/services.py       189      3    98%   78-80

Coverage Configuration

pyproject.toml
[tool.coverage.run]
include = ["footycollect/**"]
omit = ["*/migrations/*", "*/tests/*"]
plugins = ["django_coverage_plugin"]
Migrations and test files are excluded from coverage reporting. Only application code is measured.

Test Database

Pytest uses a separate test database:
pytest --reuse-db
The --reuse-db and --nomigrations flags are set by default in pytest.ini for faster test runs. The database is created from models, not migrations.

Writing Tests

Using Fixtures

Common fixtures are defined in footycollect/conftest.py:
import pytest
from footycollect.users.models import User
from footycollect.core.models import Club, Season

@pytest.fixture
def user(db):
    return User.objects.create_user(
        username="testuser",
        email="[email protected]",
        password="testpass123",
    )

@pytest.fixture
def club(db):
    return Club.objects.create(name="Test FC")

@pytest.fixture
def season(db):
    return Season.objects.create(year="2023-24")

Test Example

test_models.py
import pytest
from footycollect.collection.models import Jersey

@pytest.mark.django_db
def test_create_jersey(user, club, season):
    """Test creating a jersey item."""
    jersey = Jersey.objects.create(
        user=user,
        club=club,
        season=season,
        item_type="home",
        condition="new",
    )
    
    assert jersey.user == user
    assert jersey.club == club
    assert jersey.season == season
    assert jersey.item_type == "home"
    assert str(jersey) == f"{club.name} {season.year} Home"

Continuous Integration

Tests run automatically on GitHub Actions:
  • On every push and pull request
  • Coverage reports uploaded to Codecov
  • Must pass before merging
See the project README for CI configuration.

Next Steps

Code Quality

Code quality tools and linting

Project Structure

Understanding the codebase

Service Layer

Service layer architecture

Build docs developers (and LLMs) love