欢迎光临
我们一直在努力

【Python 测试开发】第7讲 | API 接口测试实战 - 构建可靠的接口测试体系

专栏导读:本系列文章专为 Python 测试开发工程师打造,从单元测试到自动化测试框架,循序渐进带你掌握企业级测试技能。


环境声明

  • Python 版本:Python 3.12+
  • 核心依赖:httpx 0.28+、responses 0.25+、respx 0.21+、pytest 8.0+
  • 开发工具:PyCharm / VS Code
  • 操作系统:Windows / macOS / Linux(通用)

安装依赖:

pip install httpx==0.28.* responses==0.25.* respx==0.21.* pytest==8.0.*


学习目标

完成本讲学习后,你将能够:

  • 理解 API 接口测试的核心概念和测试维度
  • 熟练使用 httpx 库发送各类 HTTP 请求
  • 掌握 API 响应的多层次断言策略
  • 使用 responses 和 respx 模拟 HTTP 响应进行单元测试
  • 实现常见的认证授权测试(Token、API Key、OAuth2)
  • 处理错误场景(超时、重试、异常)
  • 构建可复用的 API 测试框架

  • 一、API 测试基础概念

    1.1 什么是 API 测试

    API 测试(Application Programming Interface Testing)是一种直接对应用程序接口进行测试的软件测试类型。与 UI 测试不同,API 测试不关注界面展示,而是验证接口的功能、性能、安全性和可靠性。

    API 测试的优势:

    优势说明
    执行速度快 无需加载 UI,直接调用接口
    稳定性高 不受界面变动影响
    覆盖范围广 可测试异常场景和边界条件
    易于自动化 适合集成到 CI/CD 流水线

    1.2 API 测试维度

    一个完整的 API 测试应覆盖以下维度:

  • 功能测试:验证接口是否按预期工作
  • 边界测试:测试参数边界值(空值、超长字符串、特殊字符)
  • 安全测试:认证、授权、SQL 注入、XSS 防护
  • 性能测试:响应时间、并发处理能力
  • 错误处理:异常场景下的错误码和提示信息
  • 契约测试:验证接口契约(OpenAPI/Swagger)是否被遵守

  • 二、HTTP 请求库选择

    2.1 requests vs httpx

    Python 生态中有两个主流的 HTTP 客户端库:

    特性requestshttpx
    同步支持 支持 支持
    异步支持 不支持 支持(async/await)
    HTTP/2 不支持 支持
    类型提示 有限 完整
    性能 良好 更优
    活跃维护 维护中 积极开发

    推荐选择:新项目建议使用 httpx,它完全兼容 requests 的 API 设计,同时提供更现代的特性。


    三、使用 httpx 发送请求

    3.1 GET 请求

    import httpx

    def test_get_user():
    """测试获取用户信息"""
    response = httpx.get("https://api.example.com/users/1")

    # 验证状态码
    assert response.status_code == 200

    # 解析响应数据
    data = response.json()
    assert data["id"] == 1
    assert "name" in data

    带查询参数的 GET 请求:

    def test_get_users_with_params():
    """测试带参数的查询"""
    params = {
    "page": 1,
    "limit": 10,
    "status": "active"
    }

    response = httpx.get(
    "https://api.example.com/users",
    params=params
    )

    assert response.status_code == 200
    data = response.json()
    assert len(data["items"]) <= 10

    3.2 POST 请求

    def test_create_user():
    """测试创建用户"""
    payload = {
    "name": "张三",
    "email": "zhangsan@example.com",
    "role": "user"
    }

    response = httpx.post(
    "https://api.example.com/users",
    json=payload
    )

    assert response.status_code == 201
    data = response.json()
    assert data["id"] is not None
    assert data["name"] == "张三"

    3.3 PUT 和 DELETE 请求

    def test_update_user():
    """测试更新用户信息"""
    payload = {
    "name": "张三(已修改)",
    "email": "zhangsan_new@example.com"
    }

    response = httpx.put(
    "https://api.example.com/users/1",
    json=payload
    )

    assert response.status_code == 200
    data = response.json()
    assert data["name"] == "张三(已修改)"

    def test_delete_user():
    """测试删除用户"""
    response = httpx.delete("https://api.example.com/users/1")

    # 删除成功返回 204 No Content
    assert response.status_code == 204

    3.4 请求头设置

    def test_request_with_headers():
    """测试带自定义请求头的请求"""
    headers = {
    "User-Agent": "TestClient/1.0",
    "Accept": "application/json",
    "X-Request-ID": "test-12345"
    }

    response = httpx.get(
    "https://api.example.com/users/1",
    headers=headers
    )

    assert response.status_code == 200


    四、API 响应断言策略

    4.1 状态码断言

    def test_status_code_assertions():
    """状态码断言示例"""
    response = httpx.get("https://api.example.com/users/1")

    # 基本状态码断言
    assert response.status_code == 200

    # 范围断言
    assert 200 <= response.status_code < 300

    # 常用状态码语义断言
    assert response.is_success # 2xx
    assert not response.is_error # 非 4xx/5xx

    4.2 响应体验证

    def test_response_body_validation():
    """响应体验证示例"""
    response = httpx.get("https://api.example.com/users/1")
    data = response.json()

    # 字段存在性验证
    assert "id" in data
    assert "name" in data
    assert "email" in data

    # 字段类型验证
    assert isinstance(data["id"], int)
    assert isinstance(data["name"], str)
    assert isinstance(data["email"], str)

    # 字段值验证
    assert data["id"] > 0
    assert len(data["name"]) > 0
    assert "@" in data["email"]

    # 嵌套对象验证
    if "profile" in data:
    assert "avatar" in data["profile"]

    4.3 响应头验证

    def test_response_headers():
    """响应头验证示例"""
    response = httpx.get("https://api.example.com/users/1")

    # 内容类型验证
    assert response.headers["content-type"] == "application/json"

    # 自定义头验证
    assert "X-Request-ID" in response.headers

    # 缓存头验证
    assert "cache-control" in response.headers

    4.4 JSON Schema 验证

    import jsonschema

    def test_json_schema_validation():
    """使用 JSON Schema 验证响应结构"""
    user_schema = {
    "type": "object",
    "required": ["id", "name", "email"],
    "properties": {
    "id": {"type": "integer", "minimum": 1},
    "name": {"type": "string", "minLength": 1},
    "email": {"type": "string", "format": "email"},
    "role": {"type": "string", "enum": ["admin", "user", "guest"]}
    }
    }

    response = httpx.get("https://api.example.com/users/1")
    data = response.json()

    # 验证响应符合 Schema
    jsonschema.validate(instance=data, schema=user_schema)


    五、使用 responses 模拟 HTTP 响应

    responses 是一个用于模拟 HTTP 请求的库,特别适合测试使用 requests 的代码。

    5.1 基础用法

    import responses
    import requests

    @responses.activate
    def test_mock_get_user():
    """使用 responses 模拟 GET 请求"""
    # 注册模拟响应
    responses.add(
    responses.GET,
    "https://api.example.com/users/1",
    json={"id": 1, "name": "张三", "email": "zhangsan@example.com"},
    status=200
    )

    # 发送请求
    response = requests.get("https://api.example.com/users/1")

    # 验证
    assert response.status_code == 200
    assert response.json()["name"] == "张三"

    5.2 模拟多种状态码

    @responses.activate
    def test_mock_not_found():
    """模拟 404 响应"""
    responses.add(
    responses.GET,
    "https://api.example.com/users/999",
    json={"error": "User not found"},
    status=404
    )

    response = requests.get("https://api.example.com/users/999")
    assert response.status_code == 404
    assert response.json()["error"] == "User not found"

    @responses.activate
    def test_mock_server_error():
    """模拟 500 响应"""
    responses.add(
    responses.GET,
    "https://api.example.com/users/1",
    json={"error": "Internal server error"},
    status=500
    )

    response = requests.get("https://api.example.com/users/1")
    assert response.status_code == 500

    5.3 请求断言

    @responses.activate
    def test_mock_request_assertion():
    """验证请求参数"""
    responses.add(
    responses.POST,
    "https://api.example.com/users",
    json={"id": 1, "name": "张三"},
    status=201
    )

    requests.post(
    "https://api.example.com/users",
    json={"name": "张三", "email": "zhangsan@example.com"}
    )

    # 验证请求被正确发送
    assert len(responses.calls) == 1
    request = responses.calls[0].request
    assert request.url == "https://api.example.com/users"
    assert "张三" in request.body.decode()


    六、使用 respx 模拟 httpx 响应

    respx 是专为 httpx 设计的模拟库,API 设计与 responses 类似。

    6.1 基础用法

    import httpx
    import respx
    from httpx import Response

    @respx.mock
    def test_mock_httpx_get():
    """使用 respx 模拟 httpx GET 请求"""
    # 注册路由和响应
    route = respx.get("https://api.example.com/users/1").mock(
    return_value=Response(200, json={"id": 1, "name": "张三"})
    )

    # 发送请求
    response = httpx.get("https://api.example.com/users/1")

    # 验证
    assert response.status_code == 200
    assert response.json()["name"] == "张三"
    assert route.called # 验证路由被调用

    6.2 动态响应

    @respx.mock
    def test_mock_dynamic_response():
    """根据请求返回不同响应"""
    def dynamic_response(request):
    user_id = request.url.path.split("/")[1]
    return Response(200, json={"id": int(user_id), "name": f"用户{user_id}"})

    respx.get(url__regex=r"/users/\\d+").mock(side_effect=dynamic_response)

    response = httpx.get("https://api.example.com/users/42")
    data = response.json()
    assert data["id"] == 42
    assert data["name"] == "用户42"

    6.3 异常模拟

    import httpx

    @respx.mock
    def test_mock_timeout():
    """模拟超时异常"""
    respx.get("https://api.example.com/users/1").mock(
    side_effect=httpx.TimeoutException("Request timed out")
    )

    try:
    httpx.get("https://api.example.com/users/1", timeout=5)
    assert False, "应该抛出超时异常"
    except httpx.TimeoutException:
    pass # 预期行为

    @respx.mock
    def test_mock_connection_error():
    """模拟连接错误"""
    respx.get("https://api.example.com/users/1").mock(
    side_effect=httpx.ConnectError("Connection failed")
    )

    try:
    httpx.get("https://api.example.com/users/1")
    assert False, "应该抛出连接异常"
    except httpx.ConnectError:
    pass # 预期行为


    七、认证与授权测试

    7.1 Bearer Token 认证

    def test_bearer_token_auth():
    """测试 Bearer Token 认证"""
    token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…"

    headers = {
    "Authorization": f"Bearer {token}"
    }

    response = httpx.get(
    "https://api.example.com/protected",
    headers=headers
    )

    assert response.status_code == 200

    7.2 API Key 认证

    def test_api_key_auth():
    """测试 API Key 认证(Header 方式)"""
    headers = {
    "X-API-Key": "your-api-key-here"
    }

    response = httpx.get(
    "https://api.example.com/data",
    headers=headers
    )

    assert response.status_code == 200

    def test_api_key_auth_query():
    """测试 API Key 认证(Query 参数方式)"""
    params = {
    "api_key": "your-api-key-here"
    }

    response = httpx.get(
    "https://api.example.com/data",
    params=params
    )

    assert response.status_code == 200

    7.3 认证失败测试

    @respx.mock
    def test_invalid_token():
    """测试无效 Token"""
    respx.get("https://api.example.com/protected").mock(
    return_value=httpx.Response(401, json={"error": "Invalid token"})
    )

    response = httpx.get(
    "https://api.example.com/protected",
    headers={"Authorization": "Bearer invalid-token"}
    )

    assert response.status_code == 401
    assert response.json()["error"] == "Invalid token"

    @respx.mock
    def test_expired_token():
    """测试过期 Token"""
    respx.get("https://api.example.com/protected").mock(
    return_value=httpx.Response(401, json={"error": "Token expired"})
    )

    response = httpx.get(
    "https://api.example.com/protected",
    headers={"Authorization": "Bearer expired-token"}
    )

    assert response.status_code == 401


    八、错误场景测试

    8.1 超时测试

    def test_request_timeout():
    """测试请求超时处理"""
    try:
    # 设置较短的超时时间
    response = httpx.get(
    "https://httpbin.org/delay/10",
    timeout=2.0
    )
    assert False, "应该超时"
    except httpx.TimeoutException as e:
    assert "timed out" in str(e).lower()

    8.2 重试机制测试

    from httpx import Client, RequestError

    def test_retry_mechanism():
    """测试重试机制"""
    # 使用 httpx 的 Transport 实现重试
    transport = httpx.HTTPTransport(retries=3)

    with httpx.Client(transport=transport) as client:
    try:
    response = client.get("https://httpbin.org/status/500")
    # 验证重试后仍返回错误
    assert response.status_code == 500
    except RequestError:
    pass

    8.3 网络异常处理

    def test_network_error_handling():
    """测试网络错误处理"""
    try:
    response = httpx.get("http://localhost:99999")
    except httpx.ConnectError:
    # 预期行为:连接被拒绝
    pass
    except httpx.NetworkError as e:
    # 其他网络错误
    print(f"Network error: {e}")


    九、API 测试框架封装

    9.1 BaseAPITest 基类

    import httpx
    from typing import Optional, Dict, Any

    class BaseAPITest:
    """API 测试基类"""

    base_url: str = ""
    default_headers: Dict[str, str] = {}

    def __init__(self):
    self.client = httpx.Client(
    base_url=self.base_url,
    headers=self.default_headers,
    timeout=30.0
    )

    def request(
    self,
    method: str,
    path: str,
    **kwargs
    ) > httpx.Response:
    """发送 HTTP 请求"""
    return self.client.request(method, path, **kwargs)

    def get(self, path: str, **kwargs) > httpx.Response:
    """GET 请求"""
    return self.request("GET", path, **kwargs)

    def post(self, path: str, **kwargs) > httpx.Response:
    """POST 请求"""
    return self.request("POST", path, **kwargs)

    def put(self, path: str, **kwargs) > httpx.Response:
    """PUT 请求"""
    return self.request("PUT", path, **kwargs)

    def delete(self, path: str, **kwargs) > httpx.Response:
    """DELETE 请求"""
    return self.request("DELETE", path, **kwargs)

    def assert_response(
    self,
    response: httpx.Response,
    expected_status: int = 200,
    expected_keys: Optional[list] = None
    ):
    """通用响应断言"""
    assert response.status_code == expected_status, \\
    f"Expected {expected_status}, got {response.status_code}"

    if expected_keys:
    data = response.json()
    for key in expected_keys:
    assert key in data, f"Missing key: {key}"

    def close(self):
    """关闭客户端"""
    self.client.close()

    def __enter__(self):
    return self

    def __exit__(self, exc_type, exc_val, exc_tb):
    self.close()

    9.2 具体 API 测试类

    class UserAPITest(BaseAPITest):
    """用户 API 测试类"""

    base_url = "https://api.example.com"
    default_headers = {
    "Content-Type": "application/json",
    "Accept": "application/json"
    }

    def create_user(self, name: str, email: str) > httpx.Response:
    """创建用户"""
    return self.post(
    "/users",
    json={"name": name, "email": email}
    )

    def get_user(self, user_id: int) > httpx.Response:
    """获取用户"""
    return self.get(f"/users/{user_id}")

    def update_user(
    self,
    user_id: int,
    data: Dict[str, Any]
    ) > httpx.Response:
    """更新用户"""
    return self.put(f"/users/{user_id}", json=data)

    def delete_user(self, user_id: int) > httpx.Response:
    """删除用户"""
    return self.delete(f"/users/{user_id}")

    9.3 使用示例

    def test_user_api_flow():
    """测试用户 API 完整流程"""
    with UserAPITest() as api:
    # 创建用户
    create_response = api.create_user("李四", "lisi@example.com")
    api.assert_response(create_response, 201, ["id", "name", "email"])

    user_id = create_response.json()["id"]

    # 获取用户
    get_response = api.get_user(user_id)
    api.assert_response(get_response, 200, ["id", "name", "email"])

    # 更新用户
    update_response = api.update_user(
    user_id,
    {"name": "李四(已更新)"}
    )
    api.assert_response(update_response, 200)

    # 删除用户
    delete_response = api.delete_user(user_id)
    api.assert_response(delete_response, 204)


    十、测试数据管理

    10.1 工厂模式创建测试数据

    import factory
    from faker import Faker

    fake = Faker()

    class UserFactory(factory.Factory):
    """用户数据工厂"""
    class Meta:
    model = dict

    id = factory.Sequence(lambda n: n)
    name = factory.LazyFunction(fake.name)
    email = factory.LazyFunction(fake.email)
    role = factory.Iterator(["admin", "user", "guest"])
    status = "active"
    created_at = factory.LazyFunction(lambda: fake.iso8601())

    # 使用示例
    def test_with_factory():
    """使用工厂创建测试数据"""
    # 创建单个用户
    user = UserFactory()
    print(user)

    # 创建批量用户
    users = UserFactory.build_batch(5)
    assert len(users) == 5

    # 创建特定属性的用户
    admin = UserFactory(role="admin", status="active")
    assert admin["role"] == "admin"

    10.2 数据清理策略

    import pytest

    @pytest.fixture
    def test_user():
    """创建测试用户并在测试后清理"""
    # 前置:创建测试数据
    user_data = UserFactory()
    response = httpx.post(
    "https://api.example.com/users",
    json=user_data
    )
    user = response.json()

    yield user

    # 后置:清理测试数据
    httpx.delete(f"https://api.example.com/users/{user['id']}")

    def test_update_user(test_user):
    """使用 fixture 提供的测试用户"""
    response = httpx.put(
    f"https://api.example.com/users/{test_user['id']}",
    json={"name": "Updated Name"}
    )
    assert response.status_code == 200


    十一、实战案例:完整的 REST API 测试项目

    11.1 项目结构

    user_api_tests/
    ├── __init__.py
    ├── conftest.py # pytest 配置和 fixtures
    ├── base.py # 基类封装
    ├── factories.py # 测试数据工厂
    ├── test_users.py # 用户 API 测试
    └── test_auth.py # 认证相关测试

    11.2 conftest.py

    import pytest
    import respx
    from httpx import Response

    @pytest.fixture
    def mock_api():
    """提供模拟 API 的 fixture"""
    with respx.mock:
    yield

    @pytest.fixture
    def base_url():
    """API 基础 URL"""
    return "https://api.example.com"

    @pytest.fixture
    def auth_headers():
    """认证请求头"""
    return {
    "Authorization": "Bearer test-token",
    "Content-Type": "application/json"
    }

    11.3 test_users.py

    import httpx
    import respx
    from httpx import Response

    class TestUserAPI:
    """用户 API 测试套件"""

    @respx.mock
    def test_create_user_success(self, base_url):
    """测试成功创建用户"""
    # 模拟响应
    respx.post(f"{base_url}/users").mock(
    return_value=Response(
    201,
    json={"id": 1, "name": "张三", "email": "zhangsan@example.com"}
    )
    )

    # 发送请求
    response = httpx.post(
    f"{base_url}/users",
    json={"name": "张三", "email": "zhangsan@example.com"}
    )

    # 验证
    assert response.status_code == 201
    data = response.json()
    assert data["id"] == 1
    assert data["name"] == "张三"

    @respx.mock
    def test_get_user_success(self, base_url):
    """测试成功获取用户"""
    respx.get(f"{base_url}/users/1").mock(
    return_value=Response(
    200,
    json={"id": 1, "name": "张三", "email": "zhangsan@example.com"}
    )
    )

    response = httpx.get(f"{base_url}/users/1")

    assert response.status_code == 200
    data = response.json()
    assert data["id"] == 1

    @respx.mock
    def test_get_user_not_found(self, base_url):
    """测试获取不存在的用户"""
    respx.get(f"{base_url}/users/999").mock(
    return_value=Response(
    404,
    json={"error": "User not found", "code": "USER_NOT_FOUND"}
    )
    )

    response = httpx.get(f"{base_url}/users/999")

    assert response.status_code == 404
    data = response.json()
    assert data["code"] == "USER_NOT_FOUND"

    @respx.mock
    def test_update_user_success(self, base_url):
    """测试成功更新用户"""
    respx.put(f"{base_url}/users/1").mock(
    return_value=Response(
    200,
    json={"id": 1, "name": "张三(已更新)", "email": "zhangsan@example.com"}
    )
    )

    response = httpx.put(
    f"{base_url}/users/1",
    json={"name": "张三(已更新)"}
    )

    assert response.status_code == 200
    assert response.json()["name"] == "张三(已更新)"

    @respx.mock
    def test_delete_user_success(self, base_url):
    """测试成功删除用户"""
    respx.delete(f"{base_url}/users/1").mock(
    return_value=Response(204)
    )

    response = httpx.delete(f"{base_url}/users/1")

    assert response.status_code == 204

    @respx.mock
    def test_create_user_validation_error(self, base_url):
    """测试创建用户参数验证失败"""
    respx.post(f"{base_url}/users").mock(
    return_value=Response(
    400,
    json={
    "error": "Validation failed",
    "details": [{"field": "email", "message": "Invalid email format"}]
    }
    )
    )

    response = httpx.post(
    f"{base_url}/users",
    json={"name": "张三", "email": "invalid-email"}
    )

    assert response.status_code == 400
    data = response.json()
    assert data["error"] == "Validation failed"
    assert data["details"][0]["field"] == "email"

    11.4 运行测试

    # 运行所有测试
    pytest user_api_tests/ -v

    # 运行特定测试文件
    pytest user_api_tests/test_users.py -v

    # 生成测试报告
    pytest user_api_tests/ -v –html=report.html


    避坑小贴士

  • 不要测试第三方服务
    使用 responses/respx 模拟外部 API,避免测试依赖网络的外部服务。

  • 注意请求超时设置
    默认超时可能过长,建议显式设置合理的 timeout 值。

  • 验证请求内容
    不仅要验证响应,还要验证发送的请求参数是否正确。

  • 清理测试数据
    使用 pytest fixture 的 yield 模式确保测试数据被清理。

  • 区分单元测试和集成测试
    单元测试使用模拟,集成测试连接真实服务,用标记区分。

  • 敏感信息处理
    不要在代码中硬编码 API Key 或 Token,使用环境变量。


  • 本章小结

    本讲我们系统学习了 API 接口测试的核心知识:

  • HTTP 请求:使用 httpx 发送 GET/POST/PUT/DELETE 请求
  • 响应断言:状态码、响应体、响应头的多层次验证
  • 模拟测试:使用 responses 和 respx 模拟 HTTP 响应
  • 认证测试:Bearer Token、API Key 的测试方法
  • 错误处理:超时、重试、异常的测试策略
  • 框架封装:构建可复用的 BaseAPITest 基类
  • 数据管理:工厂模式创建测试数据,fixture 清理资源
  • 掌握这些技能后,你可以为任何 REST API 构建可靠的自动化测试体系。


    课后练习

  • 基础练习:为以下 API 端点编写测试用例

    • GET /products – 获取商品列表
    • POST /orders – 创建订单
    • GET /orders/{id} – 查询订单状态
  • 进阶练习:实现一个支持以下功能的 API 测试框架

    • 自动重试机制
    • 请求/响应日志记录
    • 性能指标收集(响应时间)
  • 挑战练习:使用 pytest-asyncio 编写异步 API 测试


  • 下一篇预告

    【Python 测试开发】第8讲 | 数据库测试与数据验证 – 掌握 SQL 和 NoSQL 数据库的测试技巧

    我们将学习:

    • 使用 pytest 进行数据库测试
    • SQL 数据库测试(SQLite、MySQL、PostgreSQL)
    • NoSQL 数据库测试(MongoDB、Redis)
    • 数据库事务和回滚策略
    • 数据一致性和完整性验证

    如果本文对你有帮助,欢迎点赞、收藏、评论交流。关注专栏,持续学习 Python 测试开发技能。

    赞(0)
    未经允许不得转载:171主机测评 » 【Python 测试开发】第7讲 | API 接口测试实战 - 构建可靠的接口测试体系
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址