Testing with pytest (fixtures, mocks)

Tests are not bureaucracy, they're a way to change code without fear. At ProfessNet we test with pytest — with fixtures for shared state, mocks for isolation and pytest-asyncio for async code.

Anatomy of a test

The AAA convention: Arrange, Act, Assert.

def test_normalize_forest_lowercases():
    # Arrange
    raw = "  CORP.LOCAL  "
    # Act
    result = normalize_forest(raw)
    # Assert
    assert result == "corp.local"

ProfessNet standard: a test name describes the behavior: test_<what>_<condition>_<expectation>. The test file is tests/test_<module>.py.

Fixtures — shared setup

A fixture provides a ready-made object (a database session, a client, data) and cleans up after the test.

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

Parametrization

One test, many cases — without copy-paste:

@pytest.mark.parametrize(
    "raw, expected",
    [("CORP", "corp"), (" Dmz ", "dmz"), ("", "")],
)
def test_normalize_forest(raw, expected):
    assert normalize_forest(raw) == expected

Mocks — isolation from the world

Unit tests don't go over the network or the database. We replace external dependencies with a mock.

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

Async tests

We test coroutines with pytest-asyncio:

import pytest


@pytest.mark.asyncio
async def test_fetch_forest_returns_dict():
    result = await fetch_forest("corp.local")
    assert "hosts" in result

In pyproject.toml we set auto mode, so we don't have to decorate every test:

[tool.pytest.ini_options]
asyncio_mode = "auto"
addopts = "-q --cov=app --cov-report=term-missing"

Testing FastAPI endpoints

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()
TypeWhat it testsDependencies
unita single function/classeverything mocked
integrationa layer + databasea test database
API e2ean endpoint over HTTPTestClient, overrides

Tip: coverage (--cov) is an indicator, not a goal. Better 70% meaningful assertions than 100% of tests that check nothing.


Fast unit tests + a few integration tests at the boundaries = confidence when refactoring. In ZEUS a PR without tests for new logic does not pass review.