64 lines
3.0 KiB
Markdown
64 lines
3.0 KiB
Markdown
# Backend Test Import Patterns
|
|
|
|
## Problem
|
|
|
|
The `ss-tools` backend uses **relative imports** inside packages (e.g., `from ...models.task import TaskRecord` in `persistence.py`). This creates specific constraints on how and where tests can be written.
|
|
|
|
## Key Rules
|
|
|
|
### 1. Packages with `__init__.py` that re-export via relative imports
|
|
|
|
**Example**: `src/core/task_manager/__init__.py` imports `.manager` → `.persistence` → `from ...models.task` (3-level relative import).
|
|
|
|
**Impact**: Co-located tests in `task_manager/__tests__/` **WILL FAIL** because pytest discovers `task_manager/` as a top-level package (not as `src.core.task_manager`), and the 3-level `from ...` goes beyond the top-level.
|
|
|
|
**Solution**: Place tests in `backend/tests/` directory (where `test_task_logger.py` already lives). Import using `from src.core.task_manager.XXX import ...` which works because `backend/` is the pytest rootdir.
|
|
|
|
### 2. Packages WITHOUT `__init__.py`:
|
|
|
|
**Example**: `src/core/auth/` has NO `__init__.py`.
|
|
|
|
**Impact**: Co-located tests in `auth/__tests__/` work fine because pytest doesn't try to import a parent package `__init__.py`.
|
|
|
|
### 3. Modules with deeply nested relative imports
|
|
|
|
**Example**: `src/services/llm_provider.py` uses `from ..models.llm import LLMProvider` and `from ..plugins.llm_analysis.models import LLMProviderConfig`.
|
|
|
|
**Impact**: Direct import (`from src.services.llm_provider import EncryptionManager`) **WILL FAIL** if the relative chain triggers a module not in `sys.path` or if it tries to import beyond root.
|
|
|
|
**Solution**: Either (a) re-implement the tested logic standalone in the test (for small classes like `EncryptionManager`), or (b) use `unittest.mock.patch` to mock the problematic imports before importing the module.
|
|
|
|
## Working Test Locations
|
|
|
|
| Package | `__init__.py`? | Relative imports? | Co-located OK? | Test location |
|
|
|---|---|---|---|---|
|
|
| `core/task_manager/` | YES | `from ...models.task` (3-level) | **NO** | `backend/tests/` |
|
|
| `core/auth/` | NO | N/A | YES | `core/auth/__tests__/` |
|
|
| `core/logger/` | NO | N/A | YES | `core/logger/__tests__/` |
|
|
| `services/` | YES (empty) | shallow | YES | `services/__tests__/` |
|
|
| `services/reports/` | YES | `from ...core.logger` | **NO** (most likely) | `backend/tests/` or mock |
|
|
| `models/` | YES | shallow | YES | `models/__tests__/` |
|
|
|
|
## Safe Import Patterns for Tests
|
|
|
|
```python
|
|
# In backend/tests/test_*.py:
|
|
import sys
|
|
from pathlib import Path
|
|
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
|
|
|
# Then import:
|
|
from src.core.task_manager.models import Task, TaskStatus
|
|
from src.core.task_manager.persistence import TaskPersistenceService
|
|
from src.models.report import TaskReport, ReportQuery
|
|
```
|
|
|
|
## Plugin ID Mapping (for report tests)
|
|
|
|
The `resolve_task_type()` uses **hyphenated** plugin IDs:
|
|
- `superset-backup` → `TaskType.BACKUP`
|
|
- `superset-migration` → `TaskType.MIGRATION`
|
|
- `llm_dashboard_validation` → `TaskType.LLM_VERIFICATION`
|
|
- `documentation` → `TaskType.DOCUMENTATION`
|
|
- anything else → `TaskType.UNKNOWN`
|