ForcePilot/backend/test/unit/channels/infrastructure/test_dependency_injection.py
Kris 696f5f44ec chore: 完成多模块迭代优化与测试覆盖
这一批提交包含:
1. 配置项敏感字段标记与测试用例修复
2. 路由绑定乐观锁支持与静态路径校验
3. 微信WOC插件能力适配与新增单元测试
4. 多个适配器的接口对齐与测试补全
5. 新增定时任务清理处理器与依赖注入容器测试
6. 错误码体系扩展与整合测试
7. 删除临时验证脚本与代码清理
2026-07-08 12:58:43 +08:00

155 lines
5.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""yuxi.channels.infrastructure.dependency_injection 单元测试。
覆盖 ``DependencyInjectionContainer`` 行为:
- ``registerSingleton`` 注册后 ``resolve`` 返回原实例(单例语义)
- 重复注册覆盖旧绑定last-write-wins
- 未注册接口 ``resolve`` 抛 ``InternalError``(装配期漏注册视为编程错误)
- 线程安全:并发注册与解析不破坏注册表一致性
不依赖运行中的 Docker 服务,纯单元测试。
"""
from __future__ import annotations
import threading
from concurrent.futures import ThreadPoolExecutor
from unittest.mock import MagicMock
import pytest
from yuxi.channels.contract.errors import InternalError
from yuxi.channels.infrastructure.dependency_injection import (
DependencyInjectionContainer,
)
pytestmark = pytest.mark.unit
class _FakeInterface:
"""测试用接口类型占位。"""
class _FakeImpl:
"""测试用实现类型占位。"""
@pytest.mark.unit
class TestRegisterAndResolve:
"""registerSingleton + resolve 基本行为测试。"""
def test_register_and_resolve_returns_same_instance(self):
# Arrange
container = DependencyInjectionContainer()
instance = MagicMock()
# Act
container.registerSingleton(_FakeInterface, instance)
resolved = container.resolve(_FakeInterface)
# Assert
assert resolved is instance
def test_resolve_unregistered_interface_raises_internal_error(self):
# Arrange
container = DependencyInjectionContainer()
# Act / Assert: 未注册接口解析失败,错误码 INTERNAL装配期漏注册
with pytest.raises(InternalError) as exc_info:
container.resolve(_FakeInterface)
assert exc_info.value.error_code == "INTERNAL"
assert "_FakeInterface" in exc_info.value.message
def test_reregister_overrides_previous_binding(self):
"""重复注册覆盖旧绑定last-write-wins幂等覆盖语义"""
# Arrange
container = DependencyInjectionContainer()
first = MagicMock(name="first")
second = MagicMock(name="second")
# Act
container.registerSingleton(_FakeInterface, first)
container.registerSingleton(_FakeInterface, second)
resolved = container.resolve(_FakeInterface)
# Assert
assert resolved is second
assert resolved is not first
def test_multiple_interfaces_independent(self):
"""不同接口的注册互不影响。"""
# Arrange
container = DependencyInjectionContainer()
iface_a = MagicMock()
impl_b = MagicMock()
# Act
container.registerSingleton(_FakeInterface, iface_a)
container.registerSingleton(_FakeImpl, impl_b)
# Assert
assert container.resolve(_FakeInterface) is iface_a
assert container.resolve(_FakeImpl) is impl_b
@pytest.mark.unit
class TestThreadSafety:
"""容器线程安全测试。"""
def test_concurrent_register_and_resolve_consistent(self):
"""多线程并发注册与解析不破坏注册表一致性。
使用同一接口并发 register + resolve所有 resolve 必须返回注册过的
某个实例(不能是 None 或未注册异常),证明锁保护下读写串行化。
"""
# Arrange
container = DependencyInjectionContainer()
instances = [MagicMock(name=f"inst-{i}") for i in range(20)]
resolved: list[MagicMock] = []
errors: list[Exception] = []
barrier = threading.Barrier(20)
def worker(idx: int) -> None:
barrier.wait()
instance = instances[idx]
container.registerSingleton(_FakeInterface, instance)
try:
resolved.append(container.resolve(_FakeInterface))
except Exception as e: # noqa: BLE001 - 测试需捕获全部异常
errors.append(e)
# Act
with ThreadPoolExecutor(max_workers=20) as executor:
list(executor.map(worker, range(20)))
# Assert
assert not errors, f"并发 resolve 抛出异常: {errors}"
# 所有解析结果必须是注册过的某个实例last-write-wins 下最终为最后一个,
# 中间并发解析可能拿到任意已注册实例,但必须在 instances 集合中)
for r in resolved:
assert r in instances
def test_concurrent_resolve_after_register_no_error(self):
"""单次注册后并发 resolve 不抛异常且返回同一实例。"""
# Arrange
container = DependencyInjectionContainer()
instance = MagicMock()
container.registerSingleton(_FakeInterface, instance)
results: list[object] = []
errors: list[Exception] = []
barrier = threading.Barrier(50)
def worker(_) -> None:
barrier.wait()
try:
results.append(container.resolve(_FakeInterface))
except Exception as e: # noqa: BLE001
errors.append(e)
# Act
with ThreadPoolExecutor(max_workers=50) as executor:
list(executor.map(worker, range(50)))
# Assert
assert not errors
assert all(r is instance for r in results)