Documentation Index
Fetch the complete documentation index at: https://docs.fkapi.sunr4y.dev/llms.txt
Use this file to discover all available pages before exploring further.
Overview
FKApi uses pytest for testing with comprehensive coverage of API endpoints, models, services, and scrapers. This guide covers running tests, writing new tests, and maintaining test quality.
Test Configuration
Pytest Settings
Test configuration is defined in pytest.ini:
[pytest]
DJANGO_SETTINGS_MODULE = test_settings
python_files = tests.py test_*.py *_tests.py
addopts = -v --tb=short --reuse-db --nomigrations --cov=core --cov-report=term-missing --cov-report=html --cov-report=xml
testpaths = fkapi
norecursedirs = .* venv .venv
pythonpath = . fkapi
markers =
slow: marks tests as slow (deselect with '-m "not slow"')
integration: marks tests as integration tests
unit: marks tests as unit tests
Test Settings
Tests use a separate settings file (fkapi/test_settings.py) with:
- SQLite database: Faster than PostgreSQL for testing
- Local memory cache: No Redis required
- Disabled Celery: Tests run synchronously
- Simplified middleware: Only essential middleware enabled
# Database: SQLite for speed
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "test_db.sqlite3",
}
}
# Cache: Local memory
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": "unique-snowflake",
}
}
# Celery: Disabled for tests
ENABLE_CELERY = False
Running Tests
Basic Commands
# Run all tests
pytest
# Run with verbose output
pytest -v
# Run specific test file
pytest fkapi/core/tests/test_api.py
# Run specific test function
pytest fkapi/core/tests/test_api.py::test_get_kit_by_slug
# Run specific test class
pytest fkapi/core/tests/test_models.py::TestKitModel
Using Test Markers
# Run only unit tests
pytest -m unit
# Run only integration tests
pytest -m integration
# Skip slow tests
pytest -m "not slow"
# Run fast unit tests only
pytest -m "unit and not slow"
Coverage Reports
# Run with coverage (terminal output)
pytest --cov=fkapi/core --cov-report=term-missing
# Generate HTML coverage report
pytest --cov=fkapi/core --cov-report=html
# Open htmlcov/index.html in browser
# Generate XML coverage report (for CI/CD)
pytest --cov=fkapi/core --cov-report=xml
# Show lines not covered
pytest --cov=fkapi/core --cov-report=term-missing
Coverage Configuration
Coverage settings are defined in pyproject.toml:
[tool.coverage.run]
source = ["fkapi/core"]
omit = [
"*/migrations/*",
"*/tests/*",
"*/test_*.py",
"*/__pycache__/*",
"*/venv/*",
"*/.venv/*",
]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise AssertionError",
"raise NotImplementedError",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
"@abstractmethod",
]
show_missing = true
skip_covered = false
precision = 2
Test Structure
Test Organization
fkapi/core/tests/
├── __init__.py
├── test_api.py # API endpoint tests
├── test_models.py # Model tests
├── test_services.py # Service layer tests
├── test_scrapers.py # Scraper tests
├── test_parsers.py # Parser tests
├── test_cache.py # Cache utilities tests
├── test_middleware.py # Middleware tests
├── test_celery.py # Celery task tests
└── fixtures/ # Test fixtures and sample data
├── html_samples.py
└── test_data.py
Test File Template
"""
Tests for [feature name].
"""
import pytest
from django.test import TestCase
from core.models import Kit, Club
class TestFeatureName(TestCase):
"""Test suite for feature name."""
def setUp(self):
"""Set up test fixtures."""
self.club = Club.objects.create(
name="Arsenal",
slug="arsenal-kits"
)
def tearDown(self):
"""Clean up after tests."""
Kit.objects.all().delete()
Club.objects.all().delete()
def test_something(self):
"""Test description."""
# Arrange
expected = "value"
# Act
result = some_function()
# Assert
assert result == expected
@pytest.mark.slow
def test_slow_operation(self):
"""Test that takes a long time."""
# Slow test code
pass
Writing Tests
API Endpoint Tests
from django.test import Client
def test_get_kit_by_slug():
"""Test retrieving a kit by slug."""
# Arrange
client = Client()
club = Club.objects.create(name="Arsenal", slug="arsenal-kits")
kit = Kit.objects.create(
slug="arsenal-2024-home",
team=club,
name="Arsenal 2024 Home Kit"
)
# Act
response = client.get(f"/api/kits/{kit.slug}")
# Assert
assert response.status_code == 200
assert response.json()["slug"] == kit.slug
assert response.json()["name"] == kit.name
Model Tests
def test_kit_creation():
"""Test creating a Kit model."""
# Arrange
club = Club.objects.create(name="Arsenal", slug="arsenal-kits")
# Act
kit = Kit.objects.create(
slug="arsenal-2024-home",
team=club,
name="Arsenal 2024 Home Kit"
)
# Assert
assert kit.slug == "arsenal-2024-home"
assert kit.team == club
assert str(kit) == "Arsenal 2024 Home Kit"
Service Tests
from core.services.kits_service import KitsService
def test_kits_service_search():
"""Test kit search service."""
# Arrange
service = KitsService()
Club.objects.create(name="Arsenal", slug="arsenal-kits")
# Act
results = service.search_kits(query="Arsenal")
# Assert
assert len(results) > 0
assert "Arsenal" in results[0].name
Scraper Tests
from unittest.mock import Mock, patch
from core.scrapers import scrape_kit
@patch('core.scrapers.http_get')
def test_scrape_kit(mock_http_get):
"""Test kit scraping."""
# Arrange
mock_http_get.return_value = '<html>...</html>'
# Act
result = scrape_kit('arsenal-2024-home')
# Assert
assert result is not None
assert result['slug'] == 'arsenal-2024-home'
mock_http_get.assert_called_once()
Cache Tests
from django.core.cache import cache
from core.cache_utils import get_cached_kit
def test_cache_invalidation():
"""Test cache invalidation on model update."""
# Arrange
kit = Kit.objects.create(slug="test-kit", name="Test")
cached = get_cached_kit(kit.slug)
assert cached is not None
# Act
kit.name = "Updated"
kit.save()
# Assert
cache_key = f"kit_{kit.slug}"
assert cache.get(cache_key) is None
Test Fixtures
Using Pytest Fixtures
import pytest
from core.models import Club, Kit
@pytest.fixture
def sample_club():
"""Create a sample club for testing."""
return Club.objects.create(
name="Arsenal",
slug="arsenal-kits"
)
@pytest.fixture
def sample_kit(sample_club):
"""Create a sample kit for testing."""
return Kit.objects.create(
slug="arsenal-2024-home",
team=sample_club,
name="Arsenal 2024 Home Kit"
)
def test_with_fixtures(sample_kit):
"""Test using fixtures."""
assert sample_kit.slug == "arsenal-2024-home"
assert sample_kit.team.name == "Arsenal"
HTML Fixtures
Store sample HTML in fixtures/html_samples.py:
SAMPLE_KIT_PAGE = """
<html>
<body>
<h1>Arsenal 2024 Home Kit</h1>
<img src="/kits/arsenal-2024.jpg" />
</body>
</html>
"""
Best Practices
Test Naming
- Use descriptive names:
test_get_kit_returns_404_when_not_found
- Follow pattern:
test_[function]_[scenario]_[expected]
- Use docstrings to explain complex tests
Test Independence
def setUp(self):
"""Create fresh test data for each test."""
cache.clear() # Clear cache
# Create test data
def tearDown(self):
"""Clean up after each test."""
Kit.objects.all().delete()
Club.objects.all().delete()
Mocking External Services
from unittest.mock import patch
@patch('core.scrapers.requests.get')
def test_scraper_handles_timeout(mock_get):
"""Test scraper handles timeouts gracefully."""
mock_get.side_effect = TimeoutError()
result = scrape_kit('test-slug')
assert result is None
Testing Edge Cases
def test_empty_search_query():
"""Test search with empty query."""
results = search_kits(query="")
assert results == []
def test_invalid_slug():
"""Test API with invalid slug."""
response = client.get("/api/kits/invalid!!!slug")
assert response.status_code == 404
Continuous Integration
Tests run automatically on:
- Every commit (pre-commit hooks)
- Every pull request (GitHub Actions)
- Before deployment
Pre-commit Tests
# .pre-commit-config.yaml
- id: pytest-celery-tests
name: Pytest Celery Tests
entry: bash -c 'python -m pytest fkapi/core/tests/test_celery.py -v --tb=short'
language: system
pass_filenames: false
always_run: true
stages: [commit]
Coverage Goals
- Overall: >80% coverage
- Critical paths: >90% coverage (API endpoints, models)
- New code: 100% coverage required
- Excluded: Migrations, test files, admin files
Checking Coverage
# Generate coverage report
pytest --cov=fkapi/core --cov-report=term-missing
# View in browser
pytest --cov=fkapi/core --cov-report=html
open htmlcov/index.html
Troubleshooting Tests
Tests Failing Locally
# Clear test database
rm fkapi/test_db.sqlite3
# Clear cache
python -c "from django.core.cache import cache; cache.clear()"
# Reinstall dependencies
pip install -r fkapi/requirements-dev.txt
# Run tests with verbose output
pytest -vv
Import Errors
Ensure pythonpath is set correctly in pytest.ini:
Database Errors
Tests use SQLite by default. Check test_settings.py is configured correctly.
Cache Issues
Clear cache in setUp() method:
def setUp(self):
cache.clear()
Additional Resources
Remember: Good tests make confident refactoring possible!