在API自动化测试中,我们常常会遇到一个棘手的问题——接口关联。简单来说,就是后续接口的请求参数,来源于前面接口的响应。如果处理不当,测试代码很快就会变得像一团乱麻,难以维护。
今天,我们将以一个电商“购物车”的真实业务流(查->改->删)为例,从发现问题开始,一步步带你构建一个清晰、稳定且极易维护的解决方案。
一、 发现问题:动态的ID,写不死的测试数据
我们的购物车有增、删、改、查四个核心功能。测试“增加商品”很简单,因为商品ID(goodsId
)是固定的。但当我们想测试“修改”和“删除”时,问题来了:
修改购物车商品数量:API需要一个叫
id
的参数,这个id
是商品被加入购物车时,系统动态生成的唯一标识。删除购物车商品:API需要一个
productId
参数,这个我们虽然提前知道,但也必须先确认这个商品确实在购物车里。
这意味着,我们无法像登录测试那样,在 yaml
文件里预先把这些ID写死。测试“修改”和“删除”功能的前提是:购物车里必须有一个已知的商品,并且我们必须拿到它动态生成的 id
。
这个典型的“先A后B”的依赖关系,就是我们今天要解决的核心问题。
二、 解决思路:从过程式代码到声明式环境
思路一:在测试用例里“平铺直叙”(不推荐)
最直观的想法是,在一个测试函数里,按顺序执行所有操作:
调用
login()
登录。调用
add_shopping_cart()
添加一个商品。调用
query_shopping_cart()
查询购物车。从查询结果中,用循环和判断找到那个商品的
id
和productId
。调用
update_cart_item()
修改商品。调用
delete_cart_item()
删除商品。
缺点:这会导致测试用例极其臃肿,充满了大量的“准备工作”和“清理工作”代码,核心的测试逻辑被淹没。如果10个测试用例都需要这个前置条件,我们就得重复写10遍,这简直是维护性的灾难。
思路二:用Pytest Fixture“声明”我需要的环境(最佳实践)
Pytest 的 Fixture 机制正是为解决这类问题而生。我们可以把思路从“怎么做”转变为“我需要什么”。
我们不再关心如何一步步准备数据,而是直接向测试框架“声明”:“我需要一个这样的环境:一个用户已经登录,并且他的购物车里有一个已知的商品,同时请把这个商品的 id
和 productId
告诉我。”
这个负责创建和销毁特定环境的“后勤总管”,就是我们要在 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的“菜单”,即接口定义。注意,update
和 delete
的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 和关键字,清晰、高效地完成我们的测试目标。这,就是构建一个高可维护性自动化测试框架的精髓。