新增覆盖以下模块的单元测试: 1. 核心契约与校验函数 2. 持久化层工作单元与ORM映射 3. 运行时处理器注册表 4. 业务用例工具类(退避、抖动、Cron计算) 5. 基础设施容器装配 6. 框架内置清理处理器(运行日志、幂等记录) 7. 测试共享Fixture与桩实现
233 lines
6.9 KiB
Python
233 lines
6.9 KiB
Python
"""scheduler 限界上下文单元测试共享 fixture。
|
||
|
||
本文件仅放置跨多个测试文件复用的 fixture 与工厂函数。单文件独有的 helper
|
||
应保留在对应测试文件内,避免过度集中(见 testing-guidelines.md)。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from datetime import datetime
|
||
from typing import Any
|
||
from unittest.mock import AsyncMock, MagicMock
|
||
|
||
import pytest
|
||
|
||
from yuxi.scheduler.core.models import (
|
||
DailyStat,
|
||
HandlerSummary,
|
||
ScheduledTask,
|
||
ScheduledTaskRunLog,
|
||
)
|
||
|
||
|
||
# ─── 工厂函数 ─────────────────────────────────────────────────────────────
|
||
|
||
|
||
@pytest.fixture
|
||
def make_task() -> type[ScheduledTask]:
|
||
"""返回 ScheduledTask 工厂类型,调用时按需覆盖字段。
|
||
|
||
用法::
|
||
|
||
task = make_task(task_id="abc", status="paused")
|
||
"""
|
||
|
||
def _build(**overrides: Any) -> ScheduledTask:
|
||
defaults: dict[str, Any] = {
|
||
"task_id": "task-001",
|
||
"handler_name": "test_handler",
|
||
"owner_scope": "system",
|
||
"owner_id": "test:owner-1",
|
||
"schedule_kind": "cron",
|
||
"cron_expression": "*/15 * * * *",
|
||
"tz": "Asia/Shanghai",
|
||
"payload": {"key": "value"},
|
||
"enabled": True,
|
||
"delete_after_run": False,
|
||
"block_strategy": "discard_later",
|
||
"stagger_seconds": 30,
|
||
"consecutive_errors": 0,
|
||
"status": "active",
|
||
"next_run_at": datetime(2024, 1, 1, 0, 15, 0),
|
||
"id": 1,
|
||
}
|
||
defaults.update(overrides)
|
||
return ScheduledTask(**defaults)
|
||
|
||
return _build
|
||
|
||
|
||
@pytest.fixture
|
||
def make_run_log() -> type[ScheduledTaskRunLog]:
|
||
"""返回 ScheduledTaskRunLog 工厂类型。"""
|
||
|
||
def _build(**overrides: Any) -> ScheduledTaskRunLog:
|
||
defaults: dict[str, Any] = {
|
||
"task_id": "task-001",
|
||
"run_id": "run-001",
|
||
"triggered_by": "auto",
|
||
"status": "running",
|
||
"started_at": datetime(2024, 1, 1, 0, 15, 0),
|
||
"id": 1,
|
||
}
|
||
defaults.update(overrides)
|
||
return ScheduledTaskRunLog(**defaults)
|
||
|
||
return _build
|
||
|
||
|
||
@pytest.fixture
|
||
def make_handler_summary() -> type[HandlerSummary]:
|
||
"""返回 HandlerSummary 工厂类型。"""
|
||
|
||
def _build(**overrides: Any) -> HandlerSummary:
|
||
defaults: dict[str, Any] = {
|
||
"name": "test_handler",
|
||
"task_count": 5,
|
||
"last_active_at": datetime(2024, 1, 1, 0, 15, 0),
|
||
}
|
||
defaults.update(overrides)
|
||
return HandlerSummary(**defaults)
|
||
|
||
return _build
|
||
|
||
|
||
@pytest.fixture
|
||
def make_daily_stat() -> type[DailyStat]:
|
||
"""返回 DailyStat 工厂类型。"""
|
||
|
||
def _build(**overrides: Any) -> DailyStat:
|
||
defaults: dict[str, Any] = {
|
||
"stat_date": datetime(2024, 1, 1).date(),
|
||
"handler_name": "test_handler",
|
||
"success_count": 10,
|
||
"failure_count": 2,
|
||
"timeout_count": 1,
|
||
"dead_letter_count": 0,
|
||
}
|
||
defaults.update(overrides)
|
||
return DailyStat(**defaults)
|
||
|
||
return _build
|
||
|
||
|
||
# ─── Stub 端口实现 ─────────────────────────────────────────────────────────
|
||
|
||
|
||
class FakeRepositories:
|
||
"""内存版 Repositories 桩,所有方法均为 AsyncMock。
|
||
|
||
通过 ``task`` / ``run_log`` / ``run_log_daily`` / ``idempotency`` 属性
|
||
访问各仓储桩,可按需配置 ``return_value`` / ``side_effect``。
|
||
"""
|
||
|
||
def __init__(self) -> None:
|
||
self.task = AsyncMock()
|
||
self.run_log = AsyncMock()
|
||
self.run_log_daily = AsyncMock()
|
||
self.idempotency = AsyncMock()
|
||
|
||
|
||
class FakeRuntimeServices:
|
||
"""内存版 RuntimeServices 桩。
|
||
|
||
- ``handler_registry``:``MagicMock`` 或 None(模拟 api 进程)
|
||
- ``arq_pool``:``AsyncMock`` 或 None(模拟 api 进程)
|
||
- ``config``:``MagicMock``,按需配置 ``scheduler_*`` 字段
|
||
"""
|
||
|
||
def __init__(self) -> None:
|
||
self.handler_registry = MagicMock()
|
||
self.arq_pool = AsyncMock()
|
||
# 默认配置与 app.py 默认值对齐
|
||
self.config = MagicMock()
|
||
self.config.scheduler_max_payload_size_kb = 64
|
||
self.config.scheduler_min_cron_interval_minutes = 10
|
||
self.config.scheduler_stagger_max_seconds = 300
|
||
self.config.scheduler_backoff_schedule = [10, 30, 120, 600, 3600]
|
||
self.config.scheduler_max_consecutive_errors = 5
|
||
self.config.scheduler_task_timeout_seconds = 600
|
||
self.config.scheduler_run_log_retention_days = 90
|
||
self.config.scheduler_idempotency_retention_hours = 24
|
||
|
||
|
||
class FakeUnitOfWork:
|
||
"""内存版 UnitOfWork 桩,记录 commit / rollback 调用。
|
||
|
||
``async with`` 进入时返回 self,正常退出调用 ``commit``,异常退出调用
|
||
``rollback``,与 ``SqlAlchemyUnitOfWork`` 行为一致。
|
||
"""
|
||
|
||
def __init__(self) -> None:
|
||
self.committed = False
|
||
self.rolled_back = False
|
||
self.enter_count = 0
|
||
self.exit_count = 0
|
||
|
||
async def __aenter__(self) -> FakeUnitOfWork:
|
||
self.enter_count += 1
|
||
return self
|
||
|
||
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
||
self.exit_count += 1
|
||
if exc_type is not None:
|
||
self.rolled_back = True
|
||
else:
|
||
self.committed = True
|
||
|
||
async def commit(self) -> None:
|
||
self.committed = True
|
||
|
||
async def rollback(self) -> None:
|
||
self.rolled_back = True
|
||
|
||
|
||
@pytest.fixture
|
||
def fake_repos() -> FakeRepositories:
|
||
"""提供内存版 Repositories 桩。"""
|
||
return FakeRepositories()
|
||
|
||
|
||
@pytest.fixture
|
||
def fake_runtime() -> FakeRuntimeServices:
|
||
"""提供内存版 RuntimeServices 桩。"""
|
||
return FakeRuntimeServices()
|
||
|
||
|
||
@pytest.fixture
|
||
def fake_uow() -> FakeUnitOfWork:
|
||
"""提供内存版 UnitOfWork 桩。"""
|
||
return FakeUnitOfWork()
|
||
|
||
|
||
@pytest.fixture
|
||
def fixed_now(monkeypatch: pytest.MonkeyPatch) -> datetime:
|
||
"""固定 UTC naive 时间,用于时间相关测试的稳定基准。
|
||
|
||
同时 monkeypatch ``scheduler_service`` 模块内的 ``utc_now_naive``,
|
||
使被测服务消费到同一固定时间。
|
||
"""
|
||
fixed = datetime(2024, 6, 1, 0, 0, 0)
|
||
import yuxi.scheduler.use_cases.services.scheduler_service as svc_mod
|
||
|
||
monkeypatch.setattr(svc_mod, "utc_now_naive", lambda: fixed)
|
||
return fixed
|
||
|
||
|
||
@pytest.fixture
|
||
def fixed_uuid(monkeypatch: pytest.MonkeyPatch) -> str:
|
||
"""固定 ``uuid.uuid4().hex`` 返回值,保证 task_id / run_id 可重复。
|
||
|
||
被 ``scheduler_service`` 模块内的 ``uuid.uuid4`` 调用消费。
|
||
"""
|
||
fixed_hex = "fixed-uuid-hex-001"
|
||
|
||
class _FakeUUID:
|
||
hex = fixed_hex
|
||
|
||
monkeypatch.setattr(
|
||
"yuxi.scheduler.use_cases.services.scheduler_service.uuid.uuid4",
|
||
lambda: _FakeUUID(),
|
||
)
|
||
return fixed_hex
|