从“一团乱麻”到“井井有条”——用Pytest Fixture优雅解决API接口关联问题(以litemall项目为例)

作者:c_chun 发布时间: 2025-09-23 阅读量:3 评论数:0

在API自动化测试中,我们常常会遇到一个棘手的问题——接口关联。简单来说,就是后续接口的请求参数,来源于前面接口的响应。如果处理不当,测试代码很快就会变得像一团乱麻,难以维护。

今天,我们将以一个电商“购物车”的真实业务流(查->改->删)为例,从发现问题开始,一步步带你构建一个清晰、稳定且极易维护的解决方案。

一、 发现问题:动态的ID,写不死的测试数据

我们的购物车有增、删、改、查四个核心功能。测试“增加商品”很简单,因为商品ID(goodsId)是固定的。但当我们想测试“修改”和“删除”时,问题来了:

  • 修改购物车商品数量:API需要一个叫 id 的参数,这个 id 是商品被加入购物车时,系统动态生成的唯一标识。

  • 删除购物车商品:API需要一个 productId 参数,这个我们虽然提前知道,但也必须先确认这个商品确实在购物车里。

这意味着,我们无法像登录测试那样,在 yaml 文件里预先把这些ID写死。测试“修改”和“删除”功能的前提是:购物车里必须有一个已知的商品,并且我们必须拿到它动态生成的 id

这个典型的“先A后B”的依赖关系,就是我们今天要解决的核心问题。

二、 解决思路:从过程式代码到声明式环境

思路一:在测试用例里“平铺直叙”(不推荐)

最直观的想法是,在一个测试函数里,按顺序执行所有操作:

  1. 调用 login() 登录。

  2. 调用 add_shopping_cart() 添加一个商品。

  3. 调用 query_shopping_cart() 查询购物车。

  4. 从查询结果中,用循环和判断找到那个商品的 idproductId

  5. 调用 update_cart_item() 修改商品。

  6. 调用 delete_cart_item() 删除商品。

缺点:这会导致测试用例极其臃肿,充满了大量的“准备工作”和“清理工作”代码,核心的测试逻辑被淹没。如果10个测试用例都需要这个前置条件,我们就得重复写10遍,这简直是维护性的灾难。

思路二:用Pytest Fixture“声明”我需要的环境(最佳实践)

Pytest 的 Fixture 机制正是为解决这类问题而生。我们可以把思路从“怎么做”转变为“我需要什么”。

我们不再关心如何一步步准备数据,而是直接向测试框架“声明”:“我需要一个这样的环境:一个用户已经登录,并且他的购物车里有一个已知的商品,同时请把这个商品的 idproductId 告诉我。

这个负责创建和销毁特定环境的“后勤总管”,就是我们要在 conftest.py 中创建的 Fixture。它将使用 yield 关键字,完美地将前置准备数据提供后置清理分离开来。

三、 解决问题:代码实现与协同作战

现在,让我们看看代码是如何实现的。

第1步:武装我们的“服务生” - keywords.py

首先,确保我们的业务逻辑层 keywords.py 具备所有需要的技能。它负责将具体的API调用封装成易于理解的业务动作。

# 位于: 918/keywords/keywords.py
class Keywords:
    # ... (login, add_shopping_cart 等保持不变) ...

    def query_shopping_cart(self):
        # ... (省略具体实现)
        return self.api.call_interface('query_cart', headers=auth_headers)

    def update_cart_item(self, id, goodsId, productId, number):
        # ... (省略具体实现)
        return self.api.call_interface('update_cart', data=request_data, headers=auth_headers)

    def delete_cart_item(self, productIds):
        # ... (省略具体实现)
        return self.api.call_interface('delete_cart', data=request_data, headers=auth_headers)

第2步:定义接口的“菜单” - YAML 文件

对应的,我们需要在数据文件中为这些关键字提供API的“菜单”,即接口定义。注意,updatedelete 的YAML文件不需要 test_cases,因为它们的请求数据是动态的。

# 位于: 918/data/update_shoppingcart.yaml
interfaces:
  update_cart:
    url: /wx/cart/update
    method: POST
    headers:
      Content-Type: application/json
expected:
  update_success:
    errno: 0
    errmsg: 成功
# 位于: 918/data/delete_shoppingcart.yaml
interfaces:
  delete_cart:
    url: /wx/cart/delete
    method: POST
    headers:
      Content-Type: application/json
expected:
  delete_success:
    errno: 0
    errmsg: 成功

第3步:打造全能的“后勤总管” - conftest.py

这是整个解决方案的核心。我们创建一个名为 prepared_cart_item 的高级 Fixture。

# 位于: 918/tests/conftest.py
@pytest.fixture(scope="function")
def prepared_cart_item(logged_in_keywords):
    keywords = logged_in_keywords
    test_goodsId = 1009024
    test_productId = 16

    # --- 1. 前置准备 (Setup) ---
    # 先清理,再添加,确保环境干净且已知
    keywords.delete_cart_item(productIds=[test_productId])
    keywords.add_shopping_cart(test_goodsId, test_productId, 1)

    # --- 2. 提取并提供数据 (yield) ---
    # 查询并解析出动态的ID
    cart_data = keywords.query_shopping_cart()
    cart_list = cart_data.get('data', {}).get('cartList', [])
    cart_item_id = None
    for item in cart_list:
        if item.get('productId') == test_productId:
            cart_item_id = item.get('id')
            break
    
    # `yield`是分界线,它将准备好的数据提供给测试用例
    yield {
        "cart_item_id": cart_item_id,
        "goodsId": test_goodsId,
        "productId": test_productId
    }

    # --- 3. 后置清理 (Teardown) ---
    # 测试用例执行完毕后,这里的代码会自动运行
    print("\n--- 开始执行 Teardown 清理 ---")
    keywords.delete_cart_item(productIds=[test_productId])
    print("--- Teardown 清理完成 ---")

第4步:编写极简的“剧本” - test_*.py

有了强大的 Fixture,我们的测试用例(剧本)就变得异常简洁和清晰。

# 位于: 918/tests/test_api/test_update_shoppingcart.py
@allure.feature("Shopping Cart Operations")
class TestUpdateShoppingCart:
    @allure.title("测试成功修改购物车中商品的数量")
    def test_update_cart_item_number_success(self, logged_in_keywords, prepared_cart_item):
        # 1. 直接“声明”并接收准备好的服务生和商品数据
        keywords = logged_in_keywords
        item_to_update = prepared_cart_item

        # 2. 执行核心的业务测试逻辑
        json_data = keywords.update_cart_item(
            id=item_to_update['cart_item_id'],
            goodsId=item_to_update['goodsId'],
            productId=item_to_update['productId'],
            number=5  # 修改数量为5
        )
        
        # 3. 从YAML加载预期结果并断言
        # ... (省略加载和断言代码)

看!测试用例本身只关心“修改数量”这一个核心动作,所有复杂的登录、添加商品、获取ID、删除商品等操作,都已经被 conftest.py 中的 Fixture 完美封装了。删除测试 (test_delete_shoppingcart.py) 同理,只需请求同一个 Fixture 即可。

四、 总结问题:分层解耦,声明式测试

我们再回顾一下最初的问题:如何优雅地处理有依赖关系的接口测试?

通过这次重构,我们得到的答案是:通过分层和 pytest Fixture,将“过程式”的测试准备,转变为“声明式”的环境请求。

  • YAML:负责定义“是什么”(接口信息、预期结果)。

  • keywords.py:负责定义“能做什么”(封装业务动作)。

  • conftest.py:负责定义“在什么环境下做”(提供测试前的状态和测试后的清理)。

  • test_*.py:负责“做什么测试”(在指定环境下,执行业务动作,并验证结果)。

这套架构让每个组件各司其职,完美解耦。当我们面对复杂的业务流时,不再需要编写面条式的代码,而是通过组合不同的 Fixture 和关键字,清晰、高效地完成我们的测试目标。这,就是构建一个高可维护性自动化测试框架的精髓。

评论