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 istests/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()
| Type | What it tests | Dependencies |
|---|---|---|
| unit | a single function/class | everything mocked |
| integration | a layer + database | a test database |
| API e2e | an endpoint over HTTP | TestClient, 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.