SDK 测试策略:从单元测试到契约测试的全面覆盖
coomia-dip SDK 的测试策略涵盖五个层次:单元测试(逻辑正确性)、集成测试(gRPC 通信)、契约测试(API 兼容性)、端到端测试(真实场景)和性能测试(基准回归)。测试基础设施包括 Mock gRPC 服务器、Schema 固件生成器、快照测试和 CI 流水线集成。本文从测试金字塔、各层测试实现、测试基础设施到 CI/CD 集成,完整解析 SDK 的质量保障体系。
Coomia发布于 2025年10月1日7 分钟阅读
分享本文Twitter / X
“系列:S6 平台工程 · 第 17 篇 | 难度:高级 | 阅读时间:18 分钟
SDK 测试策略:从单元测试到契约测试的全面覆盖
#TL;DR
coomia-dip SDK 的测试策略涵盖五个层次:单元测试(逻辑正确性)、集成测试(gRPC 通信)、契约测试(API 兼容性)、端到端测试(真实场景)和性能测试(基准回归)。测试基础设施包括 Mock gRPC 服务器、Schema 固件生成器、快照测试和 CI 流水线集成。本文从测试金字塔、各层测试实现、测试基础设施到 CI/CD 集成,完整解析 SDK 的质量保障体系。
#1. 测试金字塔
Code
╱╲
╱ ╲ E2E 测试 (5%)
╱────╲ 真实平台, 完整流程
╱ ╲
╱────────╲ 性能测试 (5%)
╱ ╲ 基准回归, SLA 验证
╱────────────╲
╱ ╲ 契约测试 (15%)
╱────────────────╲ API 兼容性, Protobuf 契约
╱ ╲
╱────────────────────╲ 集成测试 (25%)
╱ ╲ gRPC 通信, 序列化, 拦截器
╱────────────────────────╲
╱ ╲ 单元测试 (50%)
╱────────────────────────────╲ 模型, 过滤器, 查询构建器
#2. 单元测试
#2.1 模型测试
Python
class TestOntologyModels:
"""Ontology 模型单元测试"""
def test_object_creation(self):
employee = Employee(
id="emp-001", name="张三",
department="Engineering", salary=50000,
hire_date=date(2024, 1, 15),
)
assert employee.name == "张三"
assert employee.salary == 50000
def test_object_validation(self):
with pytest.raises(ValidationError) as exc_info:
Employee(
id="emp-001", name="张三",
department="Engineering",
salary="not_a_number", # 类型错误
hire_date=date(2024, 1, 15),
)
assert "salary" in str(exc_info.value)
def test_object_serialization(self):
employee = Employee(
id="emp-001", name="张三",
department="Engineering", salary=50000,
hire_date=date(2024, 1, 15),
)
data = employee.to_dict()
restored = Employee.from_dict(data)
assert restored == employee
def test_object_equality(self):
e1 = Employee(id="emp-001", name="张三", department="Eng", salary=50000, hire_date=date(2024, 1, 1))
e2 = Employee(id="emp-001", name="张三", department="Eng", salary=50000, hire_date=date(2024, 1, 1))
assert e1 == e2
#2.2 查询构建器测试
Python
class TestQueryBuilder:
def test_simple_filter(self):
query = (
QueryBuilder("Employee")
.where(Filter("department", "eq", "Engineering"))
.build()
)
assert query.filters[0].field == "department"
assert query.filters[0].operator == "eq"
assert query.filters[0].value == "Engineering"
def test_compound_filter(self):
query = (
QueryBuilder("Employee")
.where(Filter("department", "eq", "Engineering"))
.where(Filter("salary", "gt", 50000))
.build()
)
assert len(query.filters) == 2
def test_select_fields(self):
query = (
QueryBuilder("Employee")
.select("name", "salary")
.build()
)
assert query.selected_fields == ["name", "salary"]
def test_ordering(self):
query = (
QueryBuilder("Employee")
.order_by("salary", "desc")
.build()
)
assert query.order_by[0].field == "salary"
assert query.order_by[0].direction == "desc"
def test_pagination(self):
query = (
QueryBuilder("Employee")
.limit(50)
.offset(100)
.build()
)
assert query.page_size == 50
assert query.offset == 100
#2.3 错误映射测试
Python
class TestErrorMapping:
def test_not_found_mapping(self):
grpc_error = make_grpc_error(grpc.StatusCode.NOT_FOUND, "Object not found")
sdk_error = GrpcErrorMapper().map(grpc_error)
assert isinstance(sdk_error, ObjectNotFoundError)
def test_permission_denied_mapping(self):
grpc_error = make_grpc_error(grpc.StatusCode.PERMISSION_DENIED, "Access denied")
sdk_error = GrpcErrorMapper().map(grpc_error)
assert isinstance(sdk_error, AuthorizationError)
def test_unknown_error_mapping(self):
grpc_error = make_grpc_error(grpc.StatusCode.INTERNAL, "Server error")
sdk_error = GrpcErrorMapper().map(grpc_error)
assert isinstance(sdk_error, OntoSDKError)
#3. 集成测试
#3.1 Mock gRPC 服务器
Python
class MockOntologyServer:
"""Mock gRPC 服务器用于集成测试"""
def __init__(self):
self._objects: dict[str, dict[str, dict]] = {}
self._server: grpc.aio.Server | None = None
async def start(self, port: int = 0) -> int:
self._server = grpc.aio.server()
add_OntologyServiceServicer_to_server(
MockOntologyServicer(self._objects), self._server,
)
actual_port = self._server.add_insecure_port(f"[::]:{port}")
await self._server.start()
return actual_port
async def stop(self):
await self._server.stop(grace=5)
def seed_data(self, object_type: str, objects: list[dict]):
self._objects[object_type] = {obj["id"]: obj for obj in objects}
class MockOntologyServicer(OntologyServiceServicer):
def __init__(self, objects: dict):
self._objects = objects
async def GetObject(self, request, context):
obj_type = request.object_type
obj_id = request.object_id
if obj_type not in self._objects or obj_id not in self._objects[obj_type]:
context.set_code(grpc.StatusCode.NOT_FOUND)
context.set_details(f"{obj_type} with id '{obj_id}' not found")
return GetObjectResponse()
return GetObjectResponse(
object=self._to_proto(self._objects[obj_type][obj_id]),
)
#3.2 集成测试用例
Python
class TestGrpcIntegration:
@pytest.fixture
async def mock_server(self):
server = MockOntologyServer()
server.seed_data("Employee", [
{"id": "emp-001", "name": "张三", "department": "Engineering", "salary": 50000},
{"id": "emp-002", "name": "李四", "department": "Marketing", "salary": 45000},
])
port = await server.start()
yield f"localhost:{port}"
await server.stop()
@pytest.mark.asyncio
async def test_get_object(self, mock_server):
async with AsyncOntoPlatform.connect(mock_server) as client:
employee = await client.objects.get("Employee", "emp-001")
assert employee.name == "张三"
@pytest.mark.asyncio
async def test_not_found(self, mock_server):
async with AsyncOntoPlatform.connect(mock_server) as client:
with pytest.raises(ObjectNotFoundError):
await client.objects.get("Employee", "nonexistent")
@pytest.mark.asyncio
async def test_retry_on_unavailable(self, mock_server_with_failures):
async with AsyncOntoPlatform.connect(
mock_server_with_failures, max_retries=3,
) as client:
employee = await client.objects.get("Employee", "emp-001")
assert employee is not None
#4. 契约测试
#4.1 Protobuf 契约验证
Python
class TestProtobufContract:
"""Protobuf 契约测试 - 确保 SDK 与服务端兼容"""
def test_request_message_fields(self):
"""验证请求消息包含所有必需字段"""
request = GetObjectRequest(
object_type="Employee",
object_id="emp-001",
)
assert request.HasField("object_type") or request.object_type
assert request.HasField("object_id") or request.object_id
def test_response_message_backward_compatible(self):
"""验证响应消息向后兼容"""
# 模拟旧版本响应(缺少新字段)
old_response_data = {
"object": {"id": "emp-001", "properties": {"name": "张三"}},
}
response = GetObjectResponse()
Parse(json.dumps(old_response_data), response)
assert response.object.id == "emp-001"
def test_enum_values_stable(self):
"""验证枚举值稳定不变"""
assert FilterOperator.Value("EQUALS") == 1
assert FilterOperator.Value("GREATER_THAN") == 2
assert FilterOperator.Value("LESS_THAN") == 3
#4.2 API 兼容性测试
Python
class TestAPICompatibility:
def test_sdk_supports_server_v1(self):
"""验证 SDK 兼容服务端 v1 API"""
negotiator = VersionNegotiator(sdk_version="1.2.0")
result = negotiator.negotiate(server_version="1.0.0")
assert result.compatible
def test_sdk_warns_on_deprecated_features(self):
"""验证 SDK 对废弃功能发出警告"""
negotiator = VersionNegotiator(sdk_version="2.0.0")
result = negotiator.negotiate(server_version="1.5.0")
assert result.compatible
assert len(result.warnings) > 0
#5. 端到端测试
Python
class TestEndToEnd:
"""端到端测试 - 对真实平台执行"""
@pytest.fixture
def platform_client(self):
url = os.environ.get("ONTO_PLATFORM_URL", "http://localhost:8080")
token = os.environ.get("ONTO_PLATFORM_TOKEN")
if not token:
pytest.skip("No platform token configured")
return SyncOntoPlatform.connect(url, token=token)
def test_full_crud_lifecycle(self, platform_client):
client = platform_client
# Create
employee = client.objects.create("Employee", {
"name": "E2E Test User",
"department": "Testing",
"salary": 50000,
"hire_date": "2024-01-01",
})
assert employee.id is not None
# Read
fetched = client.objects.get("Employee", employee.id)
assert fetched.name == "E2E Test User"
# Update
updated = client.objects.update("Employee", employee.id, {"salary": 55000})
assert updated.salary == 55000
# Delete
client.objects.delete("Employee", employee.id)
with pytest.raises(ObjectNotFoundError):
client.objects.get("Employee", employee.id)
def test_query_with_filters(self, platform_client):
results = platform_client.objects.list(
"Employee",
filters=[Filter("department", "eq", "Engineering")],
page_size=10,
)
assert all(e.department == "Engineering" for e in results.items)
#6. 性能测试
Python
class TestPerformance:
@pytest.mark.benchmark
def test_serialization_performance(self, benchmark):
employee = Employee(
id="emp-001", name="张三",
department="Engineering", salary=50000,
hire_date=date(2024, 1, 15),
)
result = benchmark(employee.to_protobuf)
assert result is not None
@pytest.mark.benchmark
def test_deserialization_performance(self, benchmark):
proto = make_employee_proto()
result = benchmark(Employee.from_protobuf, proto)
assert result.name == "张三"
@pytest.mark.benchmark
@pytest.mark.asyncio
async def test_concurrent_throughput(self, benchmark, mock_server):
async with AsyncOntoPlatform.connect(mock_server) as client:
async def workload():
tasks = [
client.objects.get("Employee", f"emp-{i:03d}")
for i in range(100)
]
await asyncio.gather(*tasks)
benchmark(lambda: asyncio.get_event_loop().run_until_complete(workload()))
@pytest.mark.asyncio
async def test_latency_sla(self, mock_server):
async with AsyncOntoPlatform.connect(mock_server) as client:
latencies = []
for _ in range(100):
start = time.monotonic()
await client.objects.get("Employee", "emp-001")
latencies.append(time.monotonic() - start)
p99 = sorted(latencies)[98]
assert p99 < 0.1 # P99 < 100ms
#7. 测试基础设施
#7.1 固件生成器
Python
class SchemaFixtureGenerator:
"""Schema 固件生成器"""
@staticmethod
def generate_employee_schema() -> ObjectTypeSchema:
return ObjectTypeSchema(
api_name="Employee",
properties=[
PropertySchema(name="name", data_type="string", required=True),
PropertySchema(name="department", data_type="string", required=True),
PropertySchema(name="salary", data_type="integer", required=True),
PropertySchema(name="hire_date", data_type="date", required=True),
PropertySchema(name="email", data_type="string", required=False),
],
)
@staticmethod
def generate_employee_data(count: int = 10) -> list[dict]:
return [
{
"id": f"emp-{i:03d}",
"name": f"Employee {i}",
"department": random.choice(["Engineering", "Marketing", "Sales"]),
"salary": random.randint(40000, 120000),
"hire_date": (date(2020, 1, 1) + timedelta(days=random.randint(0, 1500))).isoformat(),
}
for i in range(count)
]
#7.2 CI 集成
YAML
# .gitlab-ci.yml
sdk-test:
stage: test
script:
- pip install -e ".[dev]"
- pytest tests/unit/ -v --cov=ontology_sdk --cov-report=xml
- pytest tests/integration/ -v --cov-append
- pytest tests/contract/ -v --cov-append
coverage: '/TOTAL.*\s+(\d+%)/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
sdk-e2e:
stage: e2e
when: manual
script:
- pytest tests/e2e/ -v
variables:
ONTO_PLATFORM_URL: $STAGING_URL
ONTO_PLATFORM_TOKEN: $STAGING_TOKEN
sdk-performance:
stage: performance
script:
- pytest tests/performance/ --benchmark-json=benchmark.json
- python scripts/check_performance_regression.py benchmark.json
artifacts:
paths:
- benchmark.json
#8. 总结
coomia-dip SDK 的测试策略通过五层测试覆盖,确保 SDK 的质量和可靠性:
- 单元测试 (50%):模型验证、查询构建器、错误映射
- 集成测试 (25%):Mock gRPC 服务器验证通信正确性
- 契约测试 (15%):Protobuf 契约和 API 兼容性
- 性能测试 (5%):序列化/反序列化基准、吞吐量和延迟 SLA
- 端到端测试 (5%):对真实平台的完整 CRUD 生命周期
下一篇将探讨 coomia-dip 的 Docker Compose 部署方案。