在 pytest 中进行关联接口调用,最核心、最推荐的方式是使用 Fixtures(测试夹具)。Fixtures 是 pytest 的精髓所在,它能完美地解决接口之间数据依赖和传递的问题。

下面我将通过一个典型的场景来详细解释如何操作:“先登录获取 token,再带着 token 去请求用户信息”

核心思想

  1. 准备工作: 将前置接口(如登录)的调用封装在一个 fixture 中。
  2. 数据传递: fixture 运行后,使用 yield 关键字将需要传递给后续接口的数据(如 token、用户ID)返回给测试用例。
  3. 依赖注入: 在测试用例的参数中声明需要用到的 fixture,pytest 会自动执行它并将 yield 的数据传入。
  4. 清理工作: 如果需要,可以在 yield 之后编写清理代码(如登出、删除测试数据),确保测试环境的纯净。

场景一:登录 -> 获取用户信息

这是一个最基础的两步关联。

1. 项目准备

首先,确保你已经安装了 pytest 和 requests

pip install pytest requests

创建一个测试文件,例如 test_user_api.py

# test_user_api.py

import pytest
import requests

# 假设这是你的API基础URL
BASE_URL = "https://api.example.com"

@pytest.fixture(scope="session") # scope="session"表示这个fixture在整个测试会话中只执行一次
def login_fixture():
    """
    一个用于登录并获取token的fixture。
    这个fixture在整个测试会话(session)中只会执行一次,
    所有需要登录的测试用例都可以共享这个token。
    """
    print("\n---【Setup】执行登录操作---")
    # 1. 准备登录数据
    login_data = {
        "username": "testuser",
        "password": "testpassword"
    }
    
    # 2. 发送登录请求
    response = requests.post(f"{BASE_URL}/login", json=login_data)
    
    # 3. 在fixture中进行断言,确保前置条件成功
    assert response.status_code == 200
    response_json = response.json()
    assert "token" in response_json
    
    # 4. 提取需要传递的数据
    token = response_json["token"]
    
    # 5. 使用yield将数据传递给测试用例
    yield token
    
    # --- yield之后的部分是Teardown,在所有测试用例执行完毕后运行 ---
    print("\n---【Teardown】执行登出或清理操作---")
    # 可以在这里添加登出接口的调用
    # requests.post(f"{BASE_URL}/logout", headers={"Authorization": f"Bearer {token}"})


def test_get_user_info(login_fixture):
    """
    测试获取用户信息的接口。
    这个测试用例依赖于login_fixture。
    """
    # 1. 从fixture接收token
    # pytest会自动将login_fixture的yield值赋给同名参数`login_fixture`
    token = login_fixture
    print(f"---【Test】获取到的token是: {token[:10]}... ---")
    
    # 2. 构造请求头
    headers = {
        "Authorization": f"Bearer {token}"
    }
    
    # 3. 发送获取用户信息的请求
    response = requests.get(f"{BASE_URL}/user/profile", headers=headers)
    
    # 4. 对最终结果进行断言
    assert response.status_code == 200
    user_info = response.json()
    assert user_info["username"] == "testuser"
    assert "email" in user_info
3. 如何运行和理解

在终端中运行 pytest:

pytest -s -v
  • -s: 为了能看到 print 的输出。
  • -v: 显示详细信息。

你会看到如下的执行流程:

  1. pytest 发现 test_get_user_info 需要 login_fixture
  2. pytest 执行 login_fixture
  3. login_fixture 打印 "【Setup】执行登录操作",发送登录请求,并断言登录成功。
  4. login_fixture 通过 yield token 将 token "暂停"并传递出去。
  5. pytest 将 token 注入到 test_get_user_info 函数的 login_fixture 参数中。
  6. test_get_user_info 函数体执行,使用 token 发起第二个请求并进行断言。
  7. 当所有依赖 login_fixture 的测试都执行完毕后(因为 scope="session"),pytest会回到 login_fixture 中 yield 之后的部分,执行清理代码。

场景二:创建商品 -> 查询商品 -> 删除商品

这个场景更复杂,展示了 fixture 之间如何相互依赖,以及如何利用 fixture 的 teardown 机制来清理测试数据。

# test_product_api.py

import pytest
import requests

# 假设login_fixture定义在同项目下的conftest.py或当前文件中
# 这里为了演示,我们重新定义一个简单的
@pytest.fixture(scope="module")
def login_fixture():
    # 假设登录成功并返回了token
    yield "fake-admin-token-for-product-management"

@pytest.fixture(scope="function") # scope="function"表示每个测试函数都会重新执行一次
def create_product_fixture(login_fixture):
    """
    创建商品的fixture,它依赖于login_fixture。
    它会创建一个商品,将商品ID传递给测试用例,
    然后在测试用例执行完毕后,自动删除这个商品。
    """
    print("\n---【Setup】执行创建商品操作---")
    token = login_fixture
    headers = {"Authorization": f"Bearer {token}"}
    product_data = {
        "name": "测试商品",
        "price": 99.99
    }
    
    response = requests.post(f"{BASE_URL}/products", json=product_data, headers=headers)
    assert response.status_code == 201 # 201 Created
    
    product_id = response.json()["id"]
    print(f"---【Setup】成功创建商品,ID: {product_id}---")
    
    # 使用yield将ID传递出去
    yield product_id
    
    # --- Teardown: 测试函数执行完毕后,删除刚刚创建的商品 ---
    print(f"\n---【Teardown】执行删除商品操作,ID: {product_id}---")
    delete_response = requests.delete(f"{BASE_URL}/products/{product_id}", headers=headers)
    assert delete_response.status_code in [200, 204] # 200 OK or 204 No Content

# --- 测试用例 ---

def test_get_product_by_id(create_product_fixture, login_fixture):
    """
    测试通过ID查询商品。
    它依赖create_product_fixture来获取一个有效的商品ID。
    """
    product_id = create_product_fixture # 从fixture获取商品ID
    token = login_fixture # 也可以同时依赖登录fixture
    
    print(f"---【Test】正在查询商品,ID: {product_id}---")
    headers = {"Authorization": f"Bearer {token}"}
    response = requests.get(f"{BASE_URL}/products/{product_id}", headers=headers)
    
    assert response.status_code == 200
    assert response.json()["name"] == "测试商品"

def test_update_product(create_product_fixture, login_fixture):
    """
    测试更新商品信息。
    同样依赖create_product_fixture。
    """
    product_id = create_product_fixture
    token = login_fixture
    
    print(f"---【Test】正在更新商品,ID: {product_id}---")
    headers = {"Authorization": f"Bearer {token}"}
    update_data = {"price": 129.99}
    
    response = requests.patch(f"{BASE_URL}/products/{product_id}", json=update_data, headers=headers)
    assert response.status_code == 200
    assert response.json()["price"] == 129.99
执行流程分析 test_get_product_by_id
  1. pytest 发现测试需要 create_product_fixture 和 login_fixture
  2. create_product_fixture 又依赖 login_fixture,所以 pytest 先执行 login_fixture
  3. login_fixture 返回 token。
  4. pytest 执行 create_product_fixture,并将 token 传入。
  5. create_product_fixture 创建一个商品,yield 出商品 ID。
  6. pytest 将商品 ID 注入 test_get_product_by_id 的 create_product_fixture 参数,并将 token 注入 login_fixture 参数。
  7. 测试函数 test_get_product_by_id 执行。
  8. 测试函数执行完毕,pytest 返回到 create_product_fixture 的 yield 之后,执行删除商品的代码。
  9. 因为 create_product_fixture 的 scope 是 function,所以 test_update_product 执行时会重复上述1-8的完整流程,确保每次测试都基于一个全新的商品。

总结与最佳实践

  1. 首选 Fixture: 忘记全局变量或在类中使用 self 传递数据。Fixture 是 pytest 中处理依赖和状态传递的标准且最优雅的方式。
  2. 合理选择 scope:
    • function : 每个测试函数都执行一次。适用于需要独立、干净数据的场景(如创建/删除)。
    • class: 每个测试类执行一次。
    • module: 每个模块(.py文件)执行一次。
    • session: 整个测试会话(一次 pytest 命令)执行一次。非常适合登录、获取配置等全局性、开销大的操作。
  3. 在 Fixture 中断言: 对前置接口的响应进行断言,如果前置条件失败,测试会立即标记为 Error,而不会执行后续不必要的步骤,有助于快速定位问题。
  4. 利用 yield 实现清理: 使用 yield 不仅是为了传递数据,更是为了利用其 teardown 机制来保证测试环境的清洁,避免测试用例之间相互影响。
  5. Fixture 依赖 Fixture: 复杂的流程可以通过 fixture 链式依赖来构建,使代码结构清晰,可读性高。

更多推荐