- Add async context manager to NFOService wrapping TMDBClient + ImageDownloader - Add TMDBClient.__del__ warning when session leaks - Log exc_info on session recreation for traceback visibility - Document async-with usage in docs/DEVELOPMENT.md and docs/TESTING.md - Add unit tests covering leak detection, context-manager cleanup, and connector-closed warning Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
156 lines
4.5 KiB
Markdown
156 lines
4.5 KiB
Markdown
# Testing Documentation
|
|
|
|
## Document Purpose
|
|
|
|
This document describes the testing strategy, guidelines, and practices for the Aniworld project.
|
|
|
|
### What This Document Contains
|
|
|
|
- **Testing Strategy**: Overall approach to quality assurance
|
|
- **Test Categories**: Unit, integration, API, performance, security tests
|
|
- **Test Structure**: Organization of test files and directories
|
|
- **Writing Tests**: Guidelines for writing effective tests
|
|
- **Fixtures and Mocking**: Shared test utilities and mock patterns
|
|
- **Running Tests**: Commands and configurations
|
|
- **Coverage Requirements**: Minimum coverage thresholds
|
|
- **CI/CD Integration**: How tests run in automation
|
|
- **Test Data Management**: Managing test fixtures and data
|
|
- **Best Practices**: Do's and don'ts for testing
|
|
|
|
### What This Document Does NOT Contain
|
|
|
|
- Production deployment (see [DEPLOYMENT.md](DEPLOYMENT.md))
|
|
- Security audit procedures (see [SECURITY.md](SECURITY.md))
|
|
- Bug tracking and issue management
|
|
- Performance benchmarking results
|
|
|
|
### Target Audience
|
|
|
|
- Developers writing tests
|
|
- QA Engineers
|
|
- CI/CD Engineers
|
|
- Code reviewers
|
|
|
|
---
|
|
|
|
## Sections to Document
|
|
|
|
1. Testing Philosophy
|
|
- Test pyramid approach
|
|
- Quality gates
|
|
2. Test Categories
|
|
- Unit Tests (`tests/unit/`)
|
|
- Integration Tests (`tests/integration/`)
|
|
- API Tests (`tests/api/`)
|
|
- Frontend Tests (`tests/frontend/`)
|
|
- Performance Tests (`tests/performance/`)
|
|
- Security Tests (`tests/security/`)
|
|
3. Test Structure and Naming
|
|
- File naming conventions
|
|
- Test function naming
|
|
- Test class organization
|
|
4. Running Tests
|
|
- pytest commands
|
|
- Running specific tests
|
|
- Verbose output
|
|
- Coverage reports
|
|
5. Fixtures and Conftest
|
|
- Shared fixtures
|
|
- Database fixtures
|
|
- Mock services
|
|
6. Mocking Guidelines
|
|
- What to mock
|
|
- Mock patterns
|
|
- External service mocks
|
|
|
|
### Mocking the Download Queue
|
|
|
|
Use `MockQueueRepository` for testing download queue functionality:
|
|
|
|
```python
|
|
from src.server.models.download import DownloadItem, EpisodeIdentifier
|
|
|
|
class MockQueueRepository:
|
|
def __init__(self):
|
|
self._items: Dict[str, DownloadItem] = {}
|
|
|
|
async def save_item(self, item: DownloadItem) -> DownloadItem:
|
|
self._items[item.id] = item
|
|
return item
|
|
|
|
async def get_item(self, item_id: str) -> Optional[DownloadItem]:
|
|
return self._items.get(item_id)
|
|
|
|
async def get_all_items(self) -> List[DownloadItem]:
|
|
return list(self._items.values())
|
|
|
|
async def set_error(self, item_id: str, error: str) -> bool:
|
|
if item_id in self._items:
|
|
self._items[item_id].error = error
|
|
return True
|
|
return False
|
|
|
|
async def delete_item(self, item_id: str) -> bool:
|
|
if item_id in self._items:
|
|
del self._items[item_id]
|
|
return True
|
|
return False
|
|
|
|
async def clear_all(self) -> int:
|
|
count = len(self._items)
|
|
self._items.clear()
|
|
return count
|
|
```
|
|
|
|
**Key points:**
|
|
- The mock uses in-memory storage, no database required
|
|
- All async methods are implemented (even if just pass-through)
|
|
- `save_item` uses `item.id` as key (must be set before calling)
|
|
- Suitable for unit tests only (no persistence)
|
|
|
|
### Mocking aiohttp Sessions
|
|
|
|
When testing code that uses `aiohttp.ClientSession`:
|
|
|
|
```python
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
from aiohttp import ClientSession
|
|
|
|
# Mock aiohttp session for testing
|
|
class MockAiohttpSession:
|
|
def __init__(self):
|
|
self.closed = False
|
|
|
|
async def close(self):
|
|
self.closed = True
|
|
|
|
def get(self, url, **kwargs):
|
|
mock_response = AsyncMock()
|
|
mock_response.status = 200
|
|
mock_response.json = AsyncMock(return_value={"data": "test"})
|
|
mock_response.__aenter__ = AsyncMock(return_value=mock_response)
|
|
mock_response.__aexit__ = AsyncMock(return_value=None)
|
|
return mock_response
|
|
|
|
# Use in fixture
|
|
@pytest.fixture
|
|
async def mock_tmdb_session():
|
|
session = MockAiohttpSession()
|
|
yield session
|
|
# Cleanup verification
|
|
assert session.closed, "Session was not closed"
|
|
```
|
|
|
|
**Key points:**
|
|
- Always verify `session.closed` is `True` after context manager exits
|
|
- Mock `__aenter__` and `__aexit__` for response context managers
|
|
- Set `closed = False` on mock session for unclosed warning tests
|
|
|
|
7. Coverage Requirements
|
|
8. CI/CD Integration
|
|
9. Writing Good Tests
|
|
- Arrange-Act-Assert pattern
|
|
- Test isolation
|
|
- Edge cases
|
|
10. Common Pitfalls to Avoid
|