Testy z pytest (fixtures, mocki)
Testy to nie biurokracja, tylko sposób, żeby zmieniać kod bez strachu. W ProfessNet
testujemy pytestem — z fixturami do współdzielonego stanu, mockami do izolacji
i pytest-asyncio do kodu async.
Anatomia testu
Konwencja AAA: Arrange (przygotuj), Act (wykonaj), Assert (sprawdź).
def test_normalize_forest_lowercases():
# Arrange
raw = " CORP.LOCAL "
# Act
result = normalize_forest(raw)
# Assert
assert result == "corp.local"
Standard ProfessNet: nazwa testu opisuje zachowanie:
test_<co>_<warunek>_<oczekiwanie>. Plik testowy totests/test_<moduł>.py.
Fixtures — współdzielony setup
Fixture dostarcza gotowy obiekt (sesję bazy, klienta, dane) i sprząta po teście.
import pytest
@pytest.fixture
def probe() -> Probe:
return Probe(host="dc01", forest="corp.local")
def test_probe_is_active(probe):
assert probe.is_active() is True
Parametryzacja
Jeden test, wiele przypadków — bez kopiuj-wklej:
@pytest.mark.parametrize(
"raw, expected",
[("CORP", "corp"), (" Dmz ", "dmz"), ("", "")],
)
def test_normalize_forest(raw, expected):
assert normalize_forest(raw) == expected
Mocki — izolacja od świata
Testy jednostkowe nie chodzą po sieci ani po bazie. Zewnętrzne zależności podmieniamy mockiem.
from unittest.mock import AsyncMock
async def test_scan_enqueues_job(monkeypatch):
fake_redis = AsyncMock()
fake_redis.enqueue_job.return_value.result = AsyncMock(return_value=42)
service = InventoryService(redis=fake_redis)
job_id = await service.start("corp.local")
fake_redis.enqueue_job.assert_awaited_once_with("run_scan", "corp.local")
assert job_id == 42
Testy async
Korutyny testujemy z pytest-asyncio:
import pytest
@pytest.mark.asyncio
async def test_fetch_forest_returns_dict():
result = await fetch_forest("corp.local")
assert "hosts" in result
W pyproject.toml ustawiamy tryb auto, żeby nie dekorować każdego testu:
[tool.pytest.ini_options]
asyncio_mode = "auto"
addopts = "-q --cov=app --cov-report=term-missing"
Testy endpointów FastAPI
from fastapi.testclient import TestClient
from app.main import app
app.dependency_overrides[get_db] = fake_db
client = TestClient(app)
def test_scan_returns_202():
resp = client.post("/api/v1/inventory/scan", json={"forest": "corp.local"})
assert resp.status_code == 202
assert "job_id" in resp.json()
| Rodzaj | Co testuje | Zależności |
|---|---|---|
| jednostkowy | jedna funkcja/klasa | wszystko zmockowane |
| integracyjny | warstwa + baza | testowa baza |
| e2e API | endpoint przez HTTP | TestClient, overrides |
Wskazówka: pokrycie (
--cov) to wskaźnik, nie cel. Lepiej 70% sensownych asercji niż 100% testów, które niczego nie sprawdzają.
Szybkie testy jednostkowe + kilka integracyjnych na granicach = pewność przy refaktorze. W ZEUS-ie PR bez testów dla nowej logiki nie przechodzi review.