背景:上次那个版本太浅了
实操:OpenAPI 3.0 spec + 项目管理 API
被测对象
GET /api/v1/projects— 列表(分页)POST /api/v1/projects— 创建(支持嵌套 tasks)GET /api/v1/projects/{id}— 详情PUT /api/v1/projects/{id}— 更新DELETE /api/v1/projects/{id}— 删除
OpenAPI spec
{”openapi”: ”3.0.3”,”info”: {”title”: ”Project Task Manager API”, ”version”: ”1.0.0”},”servers”: [{”url”: ”http://localhost:5800/api/v1”}],”components”: {”securitySchemes”: {”BearerAuth”: {”type”: ”http”, ”scheme”: ”bearer”}},”schemas”: {”Project”: {”type”: ”object”,”required”: [”name”],”properties”: {”id”: {”type”: ”integer”, ”readOnly”: true},”name”: {”type”: ”string”, ”minLength”: 1, ”maxLength”: 200},”tasks”: {”type”: ”array”,”items”: {”$ref”: ”#/components/schemas/Task”}}}},”Task”: {”type”: ”object”,”properties”: {”priority”: {”type”: ”string”, ”enum”: [”low”, ”medium”, ”high”, ”urgent”]}}}}},”security”: [{”BearerAuth”: []}],”paths”: {”/projects”: {”get”: {”parameters”: [{”name”: ”page”, ”in”: ”query”, ”schema”: {”type”: ”integer”, ”minimum”: 1}},{”name”: ”limit”, ”in”: ”query”, ”schema”: {”type”: ”integer”, ”maximum”: 100}}]}}}}
生成测试
$ python api_doc_to_tests.py --spec openapi_spec.json --output tests/[OK] 已生成测试套件到 tests/conftest.py — 2429 charstest_api.py — 18560 chars, ~33 个测试Base URL: http://localhost:5800/api/v1Auth: {'name': 'BearerAuth', 'type': 'bearer'}Endpoints: 5
生成的测试套件:到底长什么样
conftest.py:5 个 fixture,env 变量驱动
BASE_URL = os.environ.get(”API_BASE_URL”, ”http://localhost:5800/api/v1”)API_TOKEN = os.environ.get(”API_TOKEN”, ”test-bearer-token”)RATE_LIMIT_RPM = int(os.environ.get(”RATE_LIMIT_RPM”, ”60”))@pytest.fixture(scope=”session”)def auth_headers() -> dict:return {”Authorization”: f”Bearer {API_TOKEN}”}@pytest.fixture(scope=”session”)def api_client(auth_headers) -> requests.Session:session = requests.Session()session.headers.update(auth_headers)return session@pytest.fixturedef request_context(api_client, base_url):created_resources = []def _request(method, path, **kwargs):return api_client.request(method, f”{base_url}{path}”, **kwargs)def _track(resource_type, resource_id):created_resources.append((resource_type, resource_id))yield {”request”: _request, ”track”: _track}for rtype, rid in reversed(created_resources):try:api_client.delete(f”{base_url}/{rtype}s/{rid}”, timeout=5)except Exception:pass
test_api.py:每个端点自动出 4-7 条测试
def test_createProject_happy_path(request_context):resp = request_context[”request”](”POST”, ”/projects”,json={'name': 'test-name', 'tasks': [{'title': 'test-title','priority': 'low', 'done': False}]})
@pytest.mark.parametrize(”page,limit,expected_code”, [(1, 10, 200),正常分页 (1, 1, 200),# limit=1 边界 (0, 10, 400),# page=0 非法 (-1, 10, 400),# 负数page (1, 0, 400),# limit=0 非法 (1, 1001, 400),# limit超上限 (99999, 10, 200),# 超出总页数返回空集])def test_listProjects_pagination_boundary(request_context, page, limit, expected_code): ...
_SCHEMA_listProjects = { 'properties': { 'data': {'items': {...}, 'type': 'array'},'limit': {'type': 'integer'}, ... }}def test_listProjects_happy_path(request_context):resp = request_context[”request”](”GET”, ”/projects”)assert resp.status_code == 200_validate_schema(resp.json(), _SCHEMA_listProjects)
def test_rate_limit_enforcement(base_url, rate_limit_rpm):session = requests.Session()session.headers.update({”Authorization”: ”Bearer valid-token-123”})responses = []for i in range(rate_limit_rpm + 5):resp = session.get(f”{base_url}/projects”, timeout=10)responses.append(resp.status_code)if resp.status_code == 429:breakassert 429 in responses
运行结果

上次那条失败的测试,这次会复现吗?
def test_updateProject_expects_400(request_context):先创建一个真实资源 create = request_context[”request”](”POST”, ”/projects”, json={”name”: ”test”}) rid = create.json().get(”id”, 1)# 再发非法 body resp = request_context[”request”](”PUT”, f”/projects/{rid}”, json={”name”: ””}) assert resp.status_code == 400
真正的翻车点:schema 校验生成有个坑
_SCHEMA_createProject = { 'properties': { 'created_at': {'format': 'date-time','readOnly': True, 'type': 'string'}, ... }}
还有哪些没覆盖
和上次对比:深度差异在哪
结论:spec 的质量决定测试的质量
夜雨聆风