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 to tests/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()
RodzajCo testujeZależności
jednostkowyjedna funkcja/klasawszystko zmockowane
integracyjnywarstwa + bazatestowa baza
e2e APIendpoint przez HTTPTestClient, 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.