Files
gh-christianlouis-dmarq/docs/development/testing.md
T

3.8 KiB

Testing

This guide covers the testing methodology for DMARQ, including unit tests, integration tests, and how to run them.

Testing Philosophy

DMARQ follows a practical testing approach:

  • Unit Tests: Test individual functions and classes in isolation
  • Integration Tests: Test API endpoints with the full FastAPI stack
  • Security Tests: Verify security controls (input validation, XXE protection, API keys)

Test Structure

backend/app/tests/
├── conftest.py              # Pytest fixtures (DB session, TestClient, ReportStore reset)
├── test_api.py              # API endpoint tests (health, domains, upload validation)
├── test_dmarc_parser.py     # DMARC XML/ZIP parser tests
├── test_models.py           # SQLAlchemy ORM model tests
├── test_report_store.py     # In-memory ReportStore tests
├── test_reports_api.py      # Reports upload and retrieval API tests
└── test_security.py         # Security: API keys, domain validation, XML security

Setting Up the Test Environment

Prerequisites

  • Python 3.13+
  • Dependencies from backend/requirements.txt

Installation

cd backend
pip install -r requirements.txt

Running Tests

All Tests

cd backend
pytest

With Coverage

pytest --cov=app --cov-report=term-missing

Specific Test File

pytest app/tests/test_dmarc_parser.py

Tests Matching a Pattern

pytest -k "parser"

HTML Coverage Report

pytest --cov=app --cov-report=html
# Open htmlcov/index.html

Key Fixtures (conftest.py)

Fixture Scope Description
test_app function Fresh FastAPI application instance
db_session function In-memory SQLite session, tables created/dropped per test
client function TestClient wired to test DB
_reset_report_store function (autouse) Clears the ReportStore singleton between tests

The db_session fixture uses sqlite:// (true in-memory) so each test gets a clean database. All ORM models are imported in conftest.py to ensure Base.metadata.create_all() knows every table.

Writing Tests

Unit Tests (no fixtures needed)

from app.utils.domain_validator import validate_domain

def test_valid_domain():
    is_valid, error, _ = validate_domain("example.com", check_dns=False)
    assert is_valid

Model Tests (use db_session)

from app.models.domain import Domain

def test_create_domain(db_session):
    domain = Domain(name="example.com", active=True)
    db_session.add(domain)
    db_session.commit()
    assert domain.id is not None

API Tests (use client)

def test_health_check(client):
    response = client.get("/api/v1/health")
    assert response.status_code == 200
    assert response.json()["status"] == "ok"

Linting Before Committing

Always run linting before committing:

black --check backend/app
isort --check-only backend/app
flake8 backend/app --max-line-length=100 --extend-ignore=E203,W503

Auto-fix formatting:

black backend/app
isort backend/app

Code Coverage Goals

  • Overall coverage: 80%+
  • Core modules: 90%+
  • New code should have 100% branch coverage

Continuous Integration

Tests run automatically on every push and PR via GitHub Actions (.github/workflows/ci.yml). See CI/CD Pipeline for a full description of every stage.

The pipeline runs in four stages:

  1. Lint (blocking gate) — Black, isort, Flake8, Pylint
  2. Testpytest with coverage; report uploaded to Codecov
  3. Security — Bandit static analysis + pip-audit dependency scan
  4. CodeQL — GitHub's semantic code analysis for Python
  5. Docker — builds and pushes to ghcr.io (main branch only)
  6. GitOps — updates the preprod Kubernetes manifest (main branch only)