155 lines
5.2 KiB
Python
155 lines
5.2 KiB
Python
|
|
"""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)
|