Files
gh-christianlouis-docuelevate/.github/instructions/testing.instructions.md
T

6.4 KiB

applyTo
applyTo
tests/**/*.py

Testing Instructions

These instructions apply to all test files in the tests/ directory.

Test Organization

  • Mirror the structure of app/ directory in tests/
  • Name test files with test_ prefix (e.g., test_api.py)
  • Group related tests in classes with Test prefix
  • Use descriptive test function names: test_<what>_<condition>_<expected>

Pytest Configuration

  • Configuration in pytest.ini
  • Run tests: pytest -v
  • With coverage: pytest --cov=app --cov-report=term-missing
  • Run specific markers: pytest -m unit or pytest -m integration

Test Markers

Use pytest markers to categorize tests:

import pytest

@pytest.mark.unit
def test_document_validation():
    """Test document validation logic."""
    pass

@pytest.mark.integration
def test_document_upload_api():
    """Test document upload endpoint."""
    pass

@pytest.mark.slow
def test_large_file_processing():
    """Test processing of large files."""
    pass

@pytest.mark.requires_external
def test_openai_integration():
    """Test OpenAI API integration."""
    pass

Available markers:

  • unit - Unit tests for individual functions/methods
  • integration - Integration tests for API endpoints and workflows
  • slow - Tests that take significant time to run
  • security - Security-related tests
  • requires_external - Tests requiring external services (OpenAI, Azure, etc.)
  • requires_db - Tests requiring database
  • requires_redis - Tests requiring Redis

Fixtures

Use pytest fixtures for test setup and teardown:

import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.database import Base

@pytest.fixture
def db_session():
    """Provide a database session for tests."""
    engine = create_engine("sqlite:///:memory:")
    Base.metadata.create_all(engine)
    Session = sessionmaker(bind=engine)
    session = Session()

    yield session

    session.close()
    Base.metadata.drop_all(engine)

@pytest.fixture
def sample_document():
    """Provide a sample document for tests."""
    return {
        "filename": "test.pdf",
        "content_type": "application/pdf",
        "size": 1024
    }

API Testing with FastAPI

Use TestClient from FastAPI:

from fastapi.testclient import TestClient
from app.main import app

client = TestClient(app)

def test_upload_document():
    """Test document upload endpoint."""
    with open("tests/fixtures/sample.pdf", "rb") as f:
        response = client.post(
            "/api/documents/upload",
            files={"file": ("test.pdf", f, "application/pdf")}
        )

    assert response.status_code == 201
    assert "id" in response.json()

Async Testing

For async code, use pytest-asyncio:

import pytest
import httpx

@pytest.mark.asyncio
async def test_async_document_processing():
    """Test async document processing."""
    async with httpx.AsyncClient(app=app, base_url="http://test") as client:
        response = await client.get("/api/documents/1")
        assert response.status_code == 200

Mocking External Services

Always mock external services in tests:

from unittest.mock import Mock, patch

@pytest.mark.unit
def test_openai_metadata_extraction(mocker):
    """Test metadata extraction with mocked OpenAI."""
    mock_response = {
        "document_type": "invoice",
        "amount": 100.00,
        "date": "2024-01-01"
    }

    mocker.patch(
        "app.utils.openai_client.extract_metadata",
        return_value=mock_response
    )

    result = extract_document_metadata("test.pdf")
    assert result["document_type"] == "invoice"

@pytest.mark.unit
def test_azure_ocr_processing(mocker):
    """Test OCR with mocked Azure service."""
    mock_text = "Sample extracted text"

    mocker.patch(
        "app.utils.azure_client.extract_text",
        return_value=mock_text
    )

    result = perform_ocr("test.pdf")
    assert result == mock_text

Database Testing

@pytest.mark.requires_db
def test_create_document(db_session):
    """Test document creation in database."""
    from app.models import Document

    doc = Document(
        filename="test.pdf",
        user_id=1,
        file_path="/tmp/test.pdf"
    )
    db_session.add(doc)
    db_session.commit()

    assert doc.id is not None
    assert doc.filename == "test.pdf"

Test Coverage Goals

  • Achieve 100% test coverage for all new code — use # pragma: no cover only for genuinely unreachable or platform-specific branches, with an inline comment explaining why
  • Enforce the threshold: pytest --cov=app --cov-fail-under=100
  • Focus on critical paths and error handling
  • Test both success and failure scenarios
  • Don't test third-party library code

Test Structure

Follow the Arrange-Act-Assert pattern:

def test_document_validation():
    """Test that invalid documents are rejected."""
    # Arrange
    invalid_document = {
        "filename": "",  # Empty filename
        "size": -1  # Invalid size
    }

    # Act
    result = validate_document(invalid_document)

    # Assert
    assert result.is_valid is False
    assert "filename" in result.errors
    assert "size" in result.errors

Parameterized Tests

Use pytest.mark.parametrize for multiple test cases:

@pytest.mark.parametrize("filename,expected", [
    ("document.pdf", True),
    ("image.jpg", True),
    ("script.exe", False),
    ("", False),
])
def test_allowed_file_types(filename, expected):
    """Test file type validation."""
    result = is_allowed_file(filename)
    assert result == expected

Test Data

  • Place test fixtures in tests/fixtures/ directory
  • Use small sample files for testing
  • Don't commit large test files
  • Clean up test files in teardown

Error Testing

Always test error conditions:

def test_missing_file_raises_error():
    """Test that missing files raise appropriate error."""
    with pytest.raises(FileNotFoundError):
        process_document("/nonexistent/file.pdf")

def test_invalid_api_request():
    """Test API error handling."""
    response = client.post("/api/documents/", json={})
    assert response.status_code == 422  # Validation error

Best Practices

  • Test one thing per test function
  • Use descriptive test names
  • Keep tests independent (no dependencies between tests)
  • Use fixtures for common setup
  • Mock external dependencies
  • Test edge cases and error conditions
  • Keep tests fast (use mocks for slow operations)
  • Clean up resources after tests