156e0ba524
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
247 lines
6.4 KiB
Markdown
247 lines
6.4 KiB
Markdown
---
|
|
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:
|
|
```python
|
|
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:
|
|
```python
|
|
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:
|
|
```python
|
|
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`:
|
|
```python
|
|
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:
|
|
```python
|
|
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
|
|
```python
|
|
@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:
|
|
```python
|
|
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:
|
|
```python
|
|
@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:
|
|
```python
|
|
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
|