156e0ba524
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
6.4 KiB
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 intests/ - Name test files with
test_prefix (e.g.,test_api.py) - Group related tests in classes with
Testprefix - 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 unitorpytest -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/methodsintegration- Integration tests for API endpoints and workflowsslow- Tests that take significant time to runsecurity- Security-related testsrequires_external- Tests requiring external services (OpenAI, Azure, etc.)requires_db- Tests requiring databaserequires_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 coveronly 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