测试指南¶
概述¶
Symphra Container 项目遵循以下测试标准:
- 单元测试覆盖率: >= 90%
- 集成测试覆盖: 所有框架集成
- 性能测试: 关键操作的性能基准
- 测试框架: pytest + pytest-asyncio + pytest-cov
单元测试¶
测试目录结构¶
tests/
├── unit/ # 单元测试
│ ├── test_container.py # 容器基础功能
│ ├── test_lifetimes.py # 生命周期管理
│ ├── test_injection.py # 依赖注入
│ ├── test_circular.py # 循环依赖处理
│ ├── test_interceptors.py # 拦截器系统
│ ├── test_decorators.py # 装饰器系统
│ └── test_async.py # 异步支持
│
├── integration/ # 集成测试
│ ├── test_fastapi.py # FastAPI 集成
│ ├── test_flask.py # Flask 集成
│ ├── test_sqlalchemy.py # SQLAlchemy 集成
│ └── test_pydantic.py # Pydantic 集成
│
├── performance/ # 性能测试
│ ├── test_resolution_speed.py
│ ├── test_memory_usage.py
│ └── test_startup_time.py
│
└── conftest.py # 测试配置和共享夹具
单元测试示例¶
# tests/unit/test_container.py
import pytest
from symphra_container import Container, Lifetime
from symphra_container.exceptions import ServiceNotFoundError
class TestContainer:
"""容器基础功能测试"""
def test_register_and_resolve_instance(self):
"""测试基础的服务注册和解析"""
# 准备
container = Container()
test_obj = object()
# 执行
container.register_instance("test_service", test_obj)
resolved = container.resolve("test_service")
# 断言
assert resolved is test_obj
def test_resolve_nonexistent_service_raises_error(self):
"""测试解析不存在的服务会抛出异常"""
container = Container()
with pytest.raises(ServiceNotFoundError) as exc_info:
container.resolve("non_existent")
assert "non_existent" in str(exc_info.value)
@pytest.mark.parametrize("lifetime", [
Lifetime.SINGLETON,
Lifetime.TRANSIENT,
Lifetime.SCOPED,
])
def test_different_lifetimes(self, lifetime):
"""测试不同的生命周期行为"""
container = Container()
class Service:
pass
container.register(Service, lifetime=lifetime)
service1 = container.resolve(Service)
service2 = container.resolve(Service)
if lifetime == Lifetime.SINGLETON:
assert service1 is service2
else:
assert service1 is not service2
class TestInjection:
"""依赖注入测试"""
def test_constructor_injection(self):
"""测试构造函数注入"""
container = Container()
class Database:
pass
class UserService:
def __init__(self, db: Database):
self.db = db
container.register(Database)
container.register(UserService)
service = container.resolve(UserService)
assert isinstance(service.db, Database)
def test_optional_dependency(self):
"""测试可选依赖的处理"""
from typing import Optional
container = Container()
class Service:
def __init__(self, optional: Optional[str] = None):
self.optional = optional
container.register(Service)
service = container.resolve(Service)
assert service.optional is None
class TestAsyncSupport:
"""异步支持测试"""
@pytest.mark.asyncio
async def test_async_factory(self):
"""测试异步工厂函数"""
container = Container()
async def create_service():
# 模拟异步操作
return "async_service"
container.register_async_factory("service", create_service)
result = await container.resolve_async("service")
assert result == "async_service"
集成测试¶
FastAPI 集成测试¶
# tests/integration/test_fastapi.py
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from symphra_container import Container, Injected
from symphra_container.integrations.fastapi import DIMiddleware
@pytest.fixture
def app_with_container():
"""创建集成 DI 容器的 FastAPI 应用"""
app = FastAPI()
container = Container()
# 注册服务
class UserService:
def get_user(self, user_id: int):
return {"id": user_id, "name": "Test User"}
container.register(UserService)
app.add_middleware(DIMiddleware, container=container)
return app, container
def test_fastapi_injection(app_with_container):
"""测试 FastAPI 中的依赖注入"""
app, container = app_with_container
@app.get("/users/{user_id}")
def get_user(user_id: int, service: "UserService" = Injected):
return service.get_user(user_id)
client = TestClient(app)
response = client.get("/users/1")
assert response.status_code == 200
assert response.json()["id"] == 1
@pytest.mark.asyncio
async def test_fastapi_async_injection():
"""测试 FastAPI 中的异步依赖注入"""
app = FastAPI()
container = Container()
class AsyncService:
async def process(self, data: str):
return f"processed: {data}"
container.register(AsyncService)
app.add_middleware(DIMiddleware, container=container)
@app.get("/process")
async def process(service: "AsyncService" = Injected):
return await service.process("test")
from fastapi.testclient import TestClient
client = TestClient(app)
response = client.get("/process")
assert response.status_code == 200
性能测试¶
性能基准测试¶
# tests/performance/test_resolution_speed.py
import pytest
import time
from symphra_container import Container, Lifetime
class TestResolutionPerformance:
"""解析性能测试"""
def test_singleton_resolution_speed(self):
"""测试单例解析的速度(应该 < 100ns)"""
container = Container()
class Service:
pass
container.register(Service, lifetime=Lifetime.SINGLETON)
# 预热
container.resolve(Service)
# 性能测试
start = time.perf_counter_ns()
for _ in range(100000):
container.resolve(Service)
duration_ns = time.perf_counter_ns() - start
avg_ns = duration_ns / 100000
print(f"平均解析时间: {avg_ns:.2f} ns")
# 断言:平均解析时间应该 < 1000 ns (1 μs)
assert avg_ns < 1000
def test_complex_dependency_resolution(self):
"""测试复杂依赖解析的速度"""
container = Container()
class A:
pass
class B:
def __init__(self, a: A):
self.a = a
class C:
def __init__(self, b: B):
self.b = b
class D:
def __init__(self, c: C):
self.c = c
container.register(A, lifetime=Lifetime.SINGLETON)
container.register(B, lifetime=Lifetime.TRANSIENT)
container.register(C, lifetime=Lifetime.TRANSIENT)
container.register(D, lifetime=Lifetime.TRANSIENT)
start = time.perf_counter_ns()
for _ in range(10000):
container.resolve(D)
duration_ns = time.perf_counter_ns() - start
avg_ns = duration_ns / 10000
print(f"复杂依赖平均解析时间: {avg_ns:.2f} ns")
# 应该在可接受范围内
assert avg_ns < 50000 # < 50 μs
class TestMemoryUsage:
"""内存使用测试"""
def test_memory_efficiency(self):
"""测试内存使用效率"""
import sys
container = Container()
# 注册 1000 个服务
for i in range(1000):
class Service:
pass
container.register(f"service_{i}", Service)
# 获取容器的大致内存占用
size = sys.getsizeof(container)
print(f"1000 个服务的容器大小: {size / 1024:.2f} KB")
# 应该在合理范围内 (< 10 MB)
assert size < 10 * 1024 * 1024
运行测试¶
快速测试¶
# 运行所有单元测试
uv run pytest tests/unit/ -v
# 运行特定测试文件
uv run pytest tests/unit/test_container.py -v
# 运行特定测试函数
uv run pytest tests/unit/test_container.py::TestContainer::test_register_and_resolve_instance -v
完整测试和覆盖率¶
# 运行所有测试并生成覆盖率报告
uv run pytest tests/ -v --cov=src/symphra_container --cov-report=html
# 查看 HTML 报告
open htmlcov/index.html # macOS
# 或
xdg-open htmlcov/index.html # Linux
# 或
start htmlcov/index.html # Windows
分类运行测试¶
# 只运行单元测试
uv run pytest tests/unit/ -v -m "not integration and not performance"
# 只运行集成测试
uv run pytest tests/integration/ -v -m "integration"
# 只运行性能测试
uv run pytest tests/performance/ -v -m "performance"
# 运行除了慢测试外的所有测试
uv run pytest tests/ -v -m "not slow"
测试覆盖率要求¶
最低要求¶
- 总体覆盖率: >= 90%
- 关键模块: 100%
container.pyinjection/lifetimes/circular/
查看详细覆盖率¶
# 查看哪些行未覆盖
uv run pytest tests/ --cov --cov-report=term-missing
# 生成 HTML 报告(更清晰)
uv run pytest tests/ --cov --cov-report=html
# 打开 htmlcov/index.html 查看具体未覆盖的代码
最佳实践¶
1. 编写清晰的测试¶
# ✅ 好的测试
def test_container_resolves_singleton_once():
"""容器应该为单例生命周期的服务返回同一实例"""
container = Container()
container.register("service", Service, lifetime=Lifetime.SINGLETON)
instance1 = container.resolve("service")
instance2 = container.resolve("service")
assert instance1 is instance2
# ❌ 不好的测试
def test_container():
container = Container()
container.register("s", Service)
assert container.resolve("s") is container.resolve("s")
2. 使用 fixtures 减少重复代码¶
@pytest.fixture
def container():
"""每个测试都会得到一个新的容器实例"""
return Container()
def test_with_fixture(container):
container.register("service", Service)
assert container.resolve("service") is not None
3. 参数化测试¶
@pytest.mark.parametrize("lifetime", [
Lifetime.SINGLETON,
Lifetime.TRANSIENT,
Lifetime.SCOPED,
])
def test_different_lifetimes(lifetime):
"""同一个测试逻辑,多个参数值"""
container = Container()
container.register(Service, lifetime=lifetime)
# 测试逻辑...
4. 异步测试¶
@pytest.mark.asyncio
async def test_async_operation():
"""异步测试需要 pytest.mark.asyncio 标记"""
container = Container()
result = await container.resolve_async(AsyncService)
assert result is not None
持续集成¶
GitHub Actions 会自动运行所有测试:
详见 .github/workflows/ci.yml。