Testovanie
Komplexný sprievodca testovaním GitPulse.
Testovacia pyramída
graph TB
subgraph "Testovacia pyramída"
E2E["E2E testy ~10%"]
INT["Integračné testy ~30%"]
UNIT["Unit testy ~60%"]
end
E2E --> |Pomalé, drahé| INT
INT --> |Rýchlejšie| UNIT
Spustenie testov
Všetky testy
| Bash |
|---|
| # S coverage
pytest --cov=app --cov-report=html
# Bez coverage (rýchlejšie)
pytest
# Verbose output
pytest -v
|
Selektívne spustenie
| Bash |
|---|
| # Len unit testy
pytest tests/unit/
# Konkrétny modul
pytest tests/unit/test_gaming_detection.py
# Konkrétny test
pytest tests/unit/test_gaming_detection.py::test_commit_burst_detection
# Podľa markeru
pytest -m "not slow"
|
Užitočné flagy
| Bash |
|---|
| # Zastaviť na prvom zlyhaní
pytest -x
# Zobraziť print() výstupy
pytest -s
# Paralelné spustenie
pytest -n auto
# Rerun failed tests
pytest --lf
# Watch mode (s pytest-watch)
ptw
|
Unit testy
Unit testy sú rýchle a testujú izolované komponenty.
Štruktúra
| Text Only |
|---|
| tests/unit/
+-- __init__.py
+-- conftest.py # Shared fixtures
+-- test_compliance_engine.py # Compliance engine testy
+-- test_gaming_detection.py # Gaming detection testy
+-- test_roles.py # Role hierarchy testy (36 testov)
+-- test_rubric_engine.py # Rubric evaluation testy
+-- test_docker_lint.py # Docker linting testy
+-- test_security.py # Security module testy
|
Príklad unit testu
| Python |
|---|
| # tests/unit/test_gaming_detection.py
import pytest
from datetime import datetime, timedelta
from app.compliance.gaming_detection import GamingDetector
from app.compliance.gaming_types import GamingIndicator
class TestCommitBurstDetection:
"""Testy pre detekciu commit burstov."""
@pytest.fixture
def detector(self):
return GamingDetector(
burst_threshold=10,
burst_window_hours=2,
)
def test_no_burst_when_commits_spread_out(self, detector):
"""Commits rozložené v čase by nemali triggernúť burst."""
commits = [
{"timestamp": datetime.now() - timedelta(hours=i)}
for i in range(10)
]
indicators = detector.detect_bursts(commits)
assert len(indicators) == 0
def test_burst_detected_when_many_commits_in_short_time(self, detector):
"""Veľa commitov v krátkom čase by malo triggernúť burst."""
now = datetime.now()
commits = [
{"timestamp": now - timedelta(minutes=i * 5)}
for i in range(15)
]
indicators = detector.detect_bursts(commits)
assert len(indicators) == 1
assert indicators[0].type == GamingIndicator.COMMIT_BURST
@pytest.mark.parametrize("commit_count,expected_bursts", [
(5, 0),
(10, 0),
(11, 1),
(25, 1),
])
def test_burst_threshold(self, detector, commit_count, expected_bursts):
"""Test rôznych počtov commitov proti threshold."""
now = datetime.now()
commits = [
{"timestamp": now - timedelta(minutes=i)}
for i in range(commit_count)
]
indicators = detector.detect_bursts(commits)
assert len(indicators) == expected_bursts
|
Mocking
| Python |
|---|
| # tests/unit/test_gitlab_service.py
from unittest.mock import AsyncMock, patch
import pytest
from app.services.gitlab_service import GitLabService
class TestGitLabService:
@pytest.fixture
def gitlab_service(self):
return GitLabService(
base_url="https://gitlab.example.com",
token="test-token"
)
@pytest.mark.asyncio
async def test_get_project(self, gitlab_service):
"""Test getting project info from GitLab API."""
mock_response = {
"id": 123,
"name": "test-project",
"path_with_namespace": "group/test-project"
}
with patch.object(
gitlab_service,
"_request",
new_callable=AsyncMock,
return_value=mock_response
):
project = await gitlab_service.get_project(123)
assert project["id"] == 123
assert project["name"] == "test-project"
|
Integračné testy
Integračné testy testujú interakciu medzi komponentami.
Database fixtures
| Python |
|---|
| # tests/conftest.py
import pytest
import pytest_asyncio
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from app.models.base import Base
@pytest.fixture(scope="session")
def event_loop():
"""Create event loop for async tests."""
import asyncio
loop = asyncio.new_event_loop()
yield loop
loop.close()
@pytest_asyncio.fixture(scope="function")
async def db_session():
"""Create test database session."""
engine = create_async_engine(
"sqlite+aiosqlite:///:memory:",
echo=False,
)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
async_session = sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False
)
async with async_session() as session:
yield session
await engine.dispose()
|
API testy
| Python |
|---|
| # tests/integration/test_api_courses.py
import pytest
from httpx import AsyncClient
from fastapi import status
from app.main import app
@pytest.mark.asyncio
class TestCoursesAPI:
@pytest_asyncio.fixture
async def client(self):
async with AsyncClient(app=app, base_url="http://test") as ac:
yield ac
async def test_create_course(self, client, auth_headers):
"""Test creating a new course."""
course_data = {
"code": "TP-2024",
"name": "Tímový projekt",
"academic_year": "2024/2025",
"semester": "winter"
}
response = await client.post(
"/api/v1/courses/",
json=course_data,
headers=auth_headers
)
assert response.status_code == status.HTTP_201_CREATED
data = response.json()
assert data["code"] == "TP-2024"
assert "id" in data
async def test_get_course_not_found(self, client, auth_headers):
"""Test 404 for non-existent course."""
response = await client.get(
"/api/v1/courses/99999",
headers=auth_headers
)
assert response.status_code == status.HTTP_404_NOT_FOUND
|
Fixtures
Zdieľané fixtures
| Python |
|---|
| # tests/conftest.py
import pytest
from datetime import datetime
from app.models.course import Course
from app.models.team import Team
from app.models.user import User
@pytest.fixture
def sample_course():
"""Factory pre vytvorenie test course."""
def _create(
code: str = "TEST-001",
name: str = "Test Course",
**kwargs
):
return Course(
code=code,
name=name,
academic_year="2024/2025",
semester="winter",
**kwargs
)
return _create
@pytest.fixture
def sample_team(sample_course):
"""Factory pre vytvorenie test team."""
def _create(
name: str = "Team Alpha",
course: Course = None,
**kwargs
):
return Team(
name=name,
course=course or sample_course(),
**kwargs
)
return _create
@pytest.fixture
def auth_headers():
"""Headers s test auth token."""
return {"Authorization": "Bearer test-token-123"}
|
Coverage
Generovanie reportu
| Bash |
|---|
| # HTML report
pytest --cov=app --cov-report=html
open htmlcov/index.html
# Terminal report
pytest --cov=app --cov-report=term-missing
# XML (pre CI)
pytest --cov=app --cov-report=xml
|
Coverage konfigurácia
| TOML |
|---|
| # pyproject.toml
[tool.coverage.run]
source = ["src/app"]
omit = [
"*/tests/*",
"*/__pycache__/*",
"*/migrations/*",
]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
"raise NotImplementedError",
"if __name__ == .__main__.:",
]
fail_under = 80
|
CI Pipeline
| YAML |
|---|
| # .gitlab-ci.yml (test stage)
test:
stage: test
image: python:3.12-slim
services:
- postgres:16-alpine
variables:
DATABASE_URL: postgresql+asyncpg://test:test@postgres/test
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: test
script:
- pip install -e ".[dev]"
- pytest --cov=app --cov-report=xml --junitxml=report.xml
coverage: '/TOTAL.*\s+(\d+%)$/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
junit: report.xml
|
Best practices
Dobré praktiky
-
Názvy testov opisujú očakávané správanie
| Python |
|---|
| # Dobre
def test_burst_detected_when_many_commits_in_short_time():
# Zle
def test_burst():
|
-
Arrange-Act-Assert pattern
| Python |
|---|
| def test_team_metrics_calculation(self, team_service):
# Arrange
team = create_team_with_commits(count=10)
# Act
metrics = team_service.calculate_metrics(team)
# Assert
assert metrics.commit_count == 10
|
-
Izolované testy
| Python |
|---|
| # Každý test má vlastnú databázu/state
@pytest.fixture
async def clean_db():
# Setup fresh DB
yield db
# Cleanup
|
Anti-patterns
| Python |
|---|
| # 1. Testy závislé na poradí
def test_create_then_read(): # Závisí na test_create
# 2. Príliš veľa assertions
def test_everything():
assert this
assert that
assert other # Rozdeliť do viacerých testov
# 3. Sleep v testoch
import time
time.sleep(5) # Použiť mock alebo async waiting
|
Ďalšie čítanie