Python+pytest构建可复用测试数据工厂:从分层架构到工程实践
1. 项目概述与核心价值
如果你和我一样,长期在一线做自动化测试,肯定遇到过这样的场景:新项目启动,吭哧吭哧写了几百个用例,结果业务逻辑一变,或者测试数据一调整,就得花大半天甚至几天去挨个修改用例里的“前置条件”和“测试数据”。更头疼的是,不同模块的用例之间,这些准备工作的代码重复率极高,但又各有各的小差异,复制粘贴一时爽,维护起来火葬场。这背后反映的,其实是自动化测试基础设施的缺失。我们往往只关注了“测试执行”这个动作本身,而忽略了让测试能够稳定、高效、可复用地“跑起来”所需要的那一整套支撑体系。
“从‘前置条件’到‘测试数据工厂’”这个标题,精准地戳中了这个痛点。它描述的不仅仅是一个技术实现,更是一种测试工程化的思维升级。我们不再把“登录”、“清理数据”、“准备订单”这些操作零散地写在每个用例的开头,而是将它们抽象、封装、管理起来,形成一个像“工厂”一样可以按需生产、灵活装配的体系。Python + pytest 是这个体系绝佳的构建工具。Python 的灵活性和丰富的生态,pytest 强大的 fixture 机制和插件体系,为我们搭建这座“工厂”提供了坚实的地基和高效的流水线。
这套体系的核心价值在于“可复用性”和“可维护性”。它把测试工程师从繁琐、重复的“泥瓦匠”工作中解放出来,让我们能更专注于测试逻辑和业务验证本身。当你的“前置条件”和“测试数据”都成了标准化的“零件”,用例就成了清晰、简洁的“装配图”,无论是新用例开发还是老用例维护,效率都能得到质的提升。接下来,我就结合自己趟过的坑和总结的经验,带你一步步拆解如何用 Python 和 pytest 打造这样一套属于你自己的、可复用的自动化测试基础。
2. 核心设计思路:从“散兵游勇”到“工业化体系”
构建测试数据工厂,首先得在思路上完成转变。我们不能满足于在每个测试文件里写几个 setup 和 teardown 函数,或者随意定义几个全局函数。那只是“散兵游勇”式的游击战。真正的“工业化体系”,需要清晰的分层、明确的职责和统一的调度。
2.1 分层架构设计
我实践下来最有效的是四层架构,自底向上分别是: 数据层、工厂层、夹具层、用例层 。
数据层 :这是原料仓库。它不关心数据怎么用,只负责数据的定义、存储和原始获取。这里可能包括:
- 静态数据:比如写死在代码里的配置字典、列表。适用于不变的基础数据,如固定的用户角色、产品分类。
- 动态数据:通过算法实时生成的数据,比如
faker库生成的随机姓名、邮箱、地址。这是保证测试独立性的关键。 - 外部数据源:从 JSON、YAML、Excel 或数据库中读取的测试数据。适合复杂且需要多人维护的数据集。
- 环境配置数据:不同环境(测试、预发、生产)的数据库地址、API 域名、账号密码等。这部分必须与业务测试数据分离。
注意 :绝对不要在用例或夹具里硬编码环境配置。我推荐使用
pytest-base-url插件管理基础URL,用python-dotenv加载.env文件来管理其他环境变量,实现配置与代码的分离。
工厂层 :这是车间的生产线。它接收数据层的“原料”,按照业务逻辑进行加工,生产出直接可用的业务实体对象。一个工厂类或函数只负责创建一种或一类业务对象。例如:
UserFactory:负责创建用户,内部会调用数据层获取用户名、密码,然后调用注册接口,返回一个包含用户ID、token等信息的用户对象。OrderFactory:负责创建订单,它可能需要依赖UserFactory先创建一个用户,再依赖ProductFactory创建商品,最后组装成订单。
工厂层的核心原则是 “开箱即用” 和 “依赖注入” 。调用 OrderFactory.create() ,就应该直接得到一个在数据库里真实存在、状态为“待支付”的订单对象,而不是一堆需要自己组装的参数。
夹具层 :这是 pytest 的舞台,也是连接工厂和用例的桥梁。我们利用 pytest 的 fixture 系统,将工厂生产出来的对象,以依赖注入的方式提供给测试用例。这一层的关键是管理生命周期和作用域。
@pytest.fixture(scope=“function”):默认作用域,每个用例运行一次。适合独立的、无状态的数据。@pytest.fixture(scope=“class”):每个测试类运行一次。@pytest.fixture(scope=“module”):每个 Python 模块(文件)运行一次。@pytest.fixture(scope=“session”):一次测试会话(即一次pytest命令执行)只运行一次。 非常适合用来做登录态保持、数据库连接池初始化等重量级、高复用操作。
在夹具里,你可以进行更灵活的装配。比如,一个 order 夹具可以依赖 user 夹具和 product 夹具,在内部调用对应的工厂方法,最终将组装好的订单对象 yield 给用例。用例完全不用关心用户和商品是怎么来的。
用例层 :这是最终的装配车间,也是最干净的一层。用例函数通过函数参数声明它需要哪些夹具(前置条件)。它的职责只有两个:1. 执行测试动作(调用API、操作UI);2. 进行断言验证。所有繁琐的准备和清理工作,都已在下层完成。
# 用例层示例:清晰、简洁
def test_order_pay(order, payment_client):
# order 夹具已经提供了一个“待支付”的订单对象
# payment_client 夹具提供了一个配置好的支付服务客户端
result = payment_client.pay(order.id, amount=order.total_amount)
assert result[“status”] == “success”
# 通常这里还会有一个数据校验的夹具被隐式调用,去数据库验证订单状态已更新
2.2 为什么是 Pytest 的 Fixture?
你可能想问,用普通的类或函数封装这些逻辑不行吗?行为可以,但 pytest fixture 提供了不可替代的三大优势:
- 依赖注入容器 :pytest 自动解析用例函数签名中的 fixture 名字,并按依赖关系树自动调用它们。你不需要在用例里写
user = UserFactory.create(),只需要在参数里写user。这实现了“声明式”的测试准备,极大提升了代码的简洁性和可读性。 - 灵活的生命周期管理 :通过
scope参数,你可以精确控制一个前置条件的创建和销毁频率。比如,初始化一个数据库连接(scope=“session”)比每个用例都连接一次要高效得多。清理工作(teardown)通过yield语法也能优雅地实现,确保资源释放。 - 可组合性与模块化 :Fixture 可以依赖其他 fixture。这使得你可以像搭积木一样构建复杂的测试场景。基础的
db_connectionfixture 可以被user_repofixture 使用,user_repo又被userfixture 使用。这种链式依赖让代码复用率达到极致。
3. 实战构建:测试数据工厂核心模块详解
理论说再多不如动手干。我们来一步步搭建这个体系的核心模块。假设我们正在测试一个电商系统。
3.1 数据层实现:多种数据源的管理
首先,在项目根目录下创建 data_sources 目录。
1. 静态数据与模型定义 ( data_sources/models.py ) : 使用 Pydantic 或 dataclasses 来定义数据模型,这能在早期就进行数据验证,避免脏数据流入测试。
from pydantic import BaseModel, EmailStr
from typing import Optional
class UserModel(BaseModel):
username: str
password: str = “Test123456” # 提供默认值
email: Optional[EmailStr] = None
phone: Optional[str] = None
class ProductModel(BaseModel):
name: str
price: float
stock: int
category_id: int
2. 动态数据生成 ( data_sources/dynamic_data.py ) : 集成 faker 库,创建数据生成器。
from faker import Faker
from .models import UserModel, ProductModel
fake = Faker(“zh_CN”) # 使用中文数据
class DynamicDataGenerator:
@staticmethod
def generate_user(**kwargs) -> UserModel:
"""生成一个随机用户模型,允许通过kwargs覆盖默认值"""
base_data = {
“username”: fake.user_name(),
“password”: “Test” + fake.numerify(“####”),
“email”: fake.email(),
“phone”: fake.phone_number()
}
base_data.update(kwargs) # 允许自定义
return UserModel(**base_data)
@staticmethod
def generate_product(**kwargs) -> ProductModel:
base_data = {
“name”: fake.word().title() + “商品”,
“price”: fake.pyfloat(positive=True, min_value=10, max_value=10000),
“stock”: fake.random_int(min=0, max=1000),
“category_id”: fake.random_int(min=1, max=10)
}
base_data.update(kwargs)
return ProductModel(**base_data)
3. 外部数据文件读取 ( data_sources/file_loader.py ) : 对于复杂的场景数据,如购物车包含多种促销规则,用 YAML 或 JSON 管理更合适。
import yaml
import json
from pathlib import Path
class DataFileLoader:
def __init__(self, base_dir: Path = Path(“test_data”)):
self.base_dir = base_dir
def load_yaml(self, file_name: str) -> dict:
file_path = self.base_dir / f“{file_name}.yaml”
with open(file_path, ‘r’, encoding=‘utf-8’) as f:
return yaml.safe_load(f)
def load_json(self, file_name: str) -> dict:
file_path = self.base_dir / f“{file_name}.json”
with open(file_path, ‘r’, encoding=‘utf-8’) as f:
return json.load(f)
# 示例 test_data/checkout_scenarios.yaml
# complex_scenario:
# user: vip_user
# products:
# - sku: “ABC123”
# quantity: 2
# - sku: “DEF456”
# quantity: 1
# coupons: [“FREESHIP”, “10OFF”]
# expected_shipping_fee: 0
# expected_total_discount: 15.5
4. 环境配置隔离 ( config 目录) : 永远不要将环境信息写在业务代码里。
# config/__init__.py
import os
from dotenv import load_dotenv
load_dotenv() # 从项目根目录的 .env 文件加载环境变量
class EnvConfig:
TEST_ENV = os.getenv(“TEST_ENV”, “staging”) # 默认预发环境
BASE_URLS = {
“staging”: “https://api-staging.example.com”,
“production”: “https://api.example.com”,
}
@property
def BASE_URL(self):
return self.BASE_URLS.get(self.TEST_ENV, self.BASE_URLS[“staging”])
DB_HOST = os.getenv(“DB_HOST_STAGING”) if TEST_ENV == “staging” else os.getenv(“DB_HOST_PROD”)
# ... 其他配置
3.2 工厂层实现:业务实体的创建与组装
创建 factories 目录。工厂类的职责是调用数据层获取原料,调用业务接口或数据库操作,生产出“活”的业务对象。
1. 基础工厂类 ( factories/base_factory.py ) : 抽象出通用逻辑,比如 API 客户端初始化、公共头部处理。
import requests
from config import EnvConfig
class BaseFactory:
def __init__(self):
self.base_url = EnvConfig.BASE_URL
self.session = requests.Session()
# 可以在这里配置公共请求头,如 Content-Type
self.session.headers.update({“Content-Type”: “application/json”})
def _post(self, endpoint, data):
url = f“{self.base_url}{endpoint}”
resp = self.session.post(url, json=data)
resp.raise_for_status() # 非200响应直接抛异常,便于测试快速失败
return resp.json()
2. 用户工厂 ( factories/user_factory.py ) :
from .base_factory import BaseFactory
from data_sources.dynamic_data import DynamicDataGenerator
from data_sources.models import UserModel
class UserFactory(BaseFactory):
def create_user(self, **overrides) -> dict:
"""
创建一个真实用户并返回用户信息字典。
这是工厂的核心方法,将数据模型转化为系统实体。
"""
# 1. 生成用户模型数据
user_model: UserModel = DynamicDataGenerator.generate_user(**overrides)
# 2. 调用系统注册接口
create_payload = user_model.dict(exclude_none=True)
response = self._post(“/api/v1/users/register”, create_payload)
# 3. 通常注册接口会返回用户ID和token,我们补充到返回对象中
user_info = user_model.dict()
user_info.update({
“id”: response[“userId”],
“token”: response[“accessToken”]
})
return user_info
def get_user_by_id(self, user_id):
"""另一个工厂方法:根据ID获取用户信息"""
# 调用查询接口...
pass
3. 订单工厂 ( factories/order_factory.py ) : 展示工厂间的依赖。订单工厂需要先有用户和商品。
from .user_factory import UserFactory
from .product_factory import ProductFactory
class OrderFactory(BaseFactory):
def __init__(self):
super().__init__()
self.user_factory = UserFactory()
self.product_factory = ProductFactory()
def create_order(self, user_override=None, product_list=None) -> dict:
"""
创建一个待支付订单。
:param user_override: 自定义用户属性
:param product_list: 自定义商品列表,默认为生成一个随机商品
:return: 订单详情字典
"""
# 1. 创建用户(依赖UserFactory)
user = self.user_factory.create_user(**(user_override or {}))
# 2. 创建商品/或使用传入的商品列表(依赖ProductFactory)
if product_list is None:
product = self.product_factory.create_product()
product_list = [{“productId”: product[“id”], “quantity”: 1}]
# 3. 设置请求头,携带用户token
self.session.headers.update({“Authorization”: f“Bearer {user[‘token’]}”})
# 4. 调用创建订单接口
order_payload = {“items”: product_list}
order_resp = self._post(“/api/v1/orders”, order_payload)
# 5. 组装完整的订单信息,方便用例使用
full_order_info = {
**order_resp,
“user”: user, # 将用户信息也附带上,用例可能需要
“items”: product_list
}
return full_order_info
实操心得 :工厂方法应返回包含完整上下文信息的字典或对象,而不仅仅是接口返回的片段。比如订单工厂返回的数据里最好包含创建它的用户信息和商品详情,这样在写断言时,用例可以直接引用
order[‘user’][‘id’],而无需再去维护额外的变量。这减少了用例对多个夹具的依赖,让用例更内聚。
3.3 夹具层实现:Pytest Fixture 的巧妙运用
创建 conftest.py 文件,这是 pytest 的本地插件文件,其中定义的 fixture 对该目录及其子目录下的所有测试文件可见。我们通常把通用的、高复用的 fixture 放在项目根目录的 conftest.py 中,把模块特有的 fixture 放在子目录的 conftest.py 里。
1. 会话级基础夹具 ( conftest.py ) :
import pytest
from factories.user_factory import UserFactory
from factories.order_factory import OrderFactory
from your_database_client import DatabaseClient # 假设的数据库客户端
@pytest.fixture(scope=“session”)
def db_client():
"""会话级数据库客户端,整个测试过程只连接一次"""
client = DatabaseClient(host=“localhost”, port=3306)
client.connect()
yield client # 测试开始前执行 connect()
client.disconnect() # 所有测试结束后执行 disconnect()
print(“数据库连接已关闭”)
@pytest.fixture(scope=“session”)
def api_client():
"""会话级API客户端,可配置基础URL和公共头"""
from your_api_client import APIClient
client = APIClient(base_url=“https://api.example.com”)
client.set_default_headers({“X-Requested-With”: “XMLHttpRequest”})
yield client
# 可能不需要清理,直接yield
@pytest.fixture
def user_factory():
"""提供一个用户工厂实例。由于工厂无状态,可以用function作用域。"""
return UserFactory()
@pytest.fixture
def order_factory():
"""提供一个订单工厂实例。"""
return OrderFactory()
2. 业务实体夹具 ( conftest.py 继续) : 这些夹具依赖上面的基础夹具和工厂,为用例提供“开箱即用”的业务对象。
@pytest.fixture
def registered_user(user_factory):
"""创建一个已注册的普通用户。每个用例一个独立用户,避免状态污染。"""
user = user_factory.create_user()
yield user
# 清理:删除该测试用户。这是一个好习惯,确保测试隔离。
# 可以通过调用后台删除接口或直接操作数据库实现。
# 这里假设 user_factory 有 delete 方法
user_factory.delete_user(user[“id”])
print(f“测试用户 {user[‘username’]} 已清理”)
@pytest.fixture
def vip_user(user_factory):
"""创建一个VIP用户。展示了如何通过工厂参数定制数据。"""
user = user_factory.create_user(level=“vip”, vip_expiry=“2099-12-31”)
yield user
user_factory.delete_user(user[“id”])
@pytest.fixture
def pending_order(order_factory, registered_user):
"""
创建一个‘待支付’状态的订单。
它依赖 registered_user fixture,pytest会自动处理这个依赖关系。
当用例需要 pending_order 时,会先执行 registered_user,拿到用户对象。
"""
# 将 registered_user 返回的用户信息传递给订单工厂
order = order_factory.create_order(user_override={“username”: registered_user[“username”]})
yield order
# 订单清理逻辑...
3. 带参数化的夹具 这是高级用法,能让一个夹具根据不同的参数生成不同的测试数据,极大减少重复代码。
@pytest.fixture(params=[“alipay”, “wechat_pay”, “credit_card”])
def payment_method(request):
"""参数化夹具,用例会分别使用三种支付方式各跑一次。"""
return request.param
@pytest.fixture
def order_with_payment(pending_order, payment_method):
"""一个更复杂的夹具,组合了订单和支付方式。"""
# 这里可以做一些关联操作,比如将支付方式绑定到订单(如果需要)
pending_order[“selected_payment”] = payment_method
return pending_order
3.4 用例层:编写清晰简洁的测试函数
最后,在 tests 目录下,我们的用例文件会变得非常清爽。
# test_order_payment.py
class TestOrderPayment:
def test_pay_order_success(self, order_with_payment, api_client):
"""
测试订单支付成功。
order_with_payment 夹具提供了一个带支付方式的待支付订单。
"""
order = order_with_payment
payment_result = api_client.post(
f“/payments/{order[‘id’]}”,
data={“method”: order[“selected_payment”]}
)
assert payment_result.status_code == 200
assert payment_result.json()[“status”] == “paid”
# 使用数据库夹具验证状态(假设有 db_client fixture)
# db_order = db_client.query_order(order[‘id’])
# assert db_order[‘status’] == ‘PAID’
def test_pay_insufficient_balance(self, vip_user, order_factory, api_client):
"""
测试余额不足支付失败。
专门为VIP用户创建一个订单,并模拟其余额不足场景。
这里没有使用 pending_order 夹具,而是直接使用工厂定制。
"""
# 工厂模式给予我们灵活性:创建一个金额巨大的订单
expensive_product = {“productId”: “premium_item”, “quantity”: 1, “price”: 1000000}
big_order = order_factory.create_order(
user_override={“username”: vip_user[“username”]},
product_list=[expensive_product]
)
payment_result = api_client.post(
f“/payments/{big_order[‘id’]}”,
data={“method”: “balance”} # 使用余额支付
)
assert payment_result.status_code == 400
assert “insufficient balance” in payment_result.json()[“message”]
可以看到,用例里几乎没有数据准备的“脏活”了。它们只需要声明需要什么( order_with_payment , vip_user ),然后专注于调用和断言。这才是自动化测试该有的样子。
4. 高级技巧与最佳实践
搭建好基础框架只是第一步,要让它在团队中稳健运行,还需要一些“内功心法”。
4.1 Fixture 的自动清理与状态隔离
测试不清理,环境两行泪。一定要为创建资源的 fixture 配上清理逻辑。
-
yieldvsaddfinalizer:yield是更简洁的方式。yield之前的代码是 setup,之后的代码是 teardown。对于需要多个清理步骤的复杂场景,可以使用request.addfinalizer注册多个清理函数。 - 清理策略 :
- 精准删除 :在 fixture 的 teardown 中,删除它创建的资源。如
registered_user所示。这是最干净的。 - 事务回滚 :如果测试数据库支持,可以在
session或module级别的 fixture 中开启一个事务,yield 之后回滚。这样所有改动都不会提交,速度极快。但要注意有些操作(如自增ID)可能不受回滚影响。 - 标记删除 :对于不能物理删除的数据(如生产镜像库),在创建时为数据打上唯一标记(如
test_id),测试结束后用一个统一的脚本清理所有带此标记的数据。
- 精准删除 :在 fixture 的 teardown 中,删除它创建的资源。如
4.2 测试数据的可读性与维护性
工厂生成随机数据保证了独立性,但有时我们需要特定的数据组合来覆盖边界情况。
- 使用
@pytest.mark.parametrize与 Fixture 结合 :这是处理不同测试数据组合的利器。import pytest @pytest.mark.parametrize(“product_data, expected_status”, [ ({“name”: “正常商品”, “price”: 100, “stock”: 10}, 201), ({“name”: “”, “price”: 100, “stock”: 10}, 400), # 名称为空 ({“name”: “高价商品”, “price”: 1000000, “stock”: 10}, 201), # 价格边界 ({“name”: “零库存商品”, “price”: 50, “stock”: 0}, 201), ]) def test_create_product_with_various_data( self, admin_user, api_client, product_data, expected_status ): """同一个测试逻辑,用多组数据驱动。""" api_client.set_token(admin_user[“token”]) resp = api_client.post(“/api/v1/products”, product_data) assert resp.status_code == expected_status - 将复杂场景数据外置到 YAML/JSON :如前所述,对于“满减+折扣+运费”这种复杂购物车场景,把数据写在文件里比写在 Python 字典里清晰得多。Fixture 可以去读取这些文件。
4.3 性能优化:Fixture 作用域与缓存
不合理的 fixture 作用域是测试套件变慢的主因之一。
- 默认用
function:除非有明确理由,否则先用function作用域。它最安全,隔离性最好。 - 大胆使用
session:对于只读的、重量级的、全局唯一的资源,如数据库连接池、HTTP 会话(只登录一次)、缓存客户端初始化,一定要用session。 - 谨慎使用
module和class:这两个作用域容易引起测试间意外的状态依赖。除非你非常确定一组测试共享的数据完全不会被修改,否则不建议使用。一个常见的合理场景是:一个测试类里的所有方法都在读同一份大的配置文件,这时可以用class作用域的 fixture 来读取并缓存它。 -
@pytest.fixture(autouse=True):自动使用的 fixture,用于那些每个测试都必须有的、但又不需要在参数中声明的设置,比如日志初始化、监控打点。慎用,因为它会隐藏依赖关系。
4.4 集成 Allure 等报告工具,让数据工厂“可视化”
Allure 报告能完美展示 fixture 的运行过程和测试数据。
- 为 Fixture 添加 Allure 步骤 :使用
@allure.step装饰你的工厂方法或 fixture 内部的关键操作。
这样在 Allure 报告中,你能清晰地看到每个用例执行前,是哪个用户被创建了,具体信息是什么。对于调试数据问题至关重要。import allure class UserFactory(BaseFactory): @allure.step(“创建测试用户 - 用户名: {username}”) def create_user(self, username=None, **overrides): # ... 创建逻辑 allure.attach(json.dumps(user_info, indent=2), “创建的用户信息”, allure.attachment_type.JSON) return user_info - 在用例中附加关键数据 :在断言前,可以把请求和响应数据附加到报告中。
def test_something(api_client): response = api_client.post(“/some/api”, data) allure.attach(json.dumps(data, indent=2), “请求数据”, allure.attachment_type.JSON) allure.attach(json.dumps(response.json(), indent=2), “响应数据”, allure.attachment_type.JSON) assert response.status_code == 200
5. 常见问题排查与经验实录
即使设计得再好,在实际落地时也难免踩坑。下面是我总结的几个典型问题及解决方案。
问题一:Fixture 依赖循环(Circular Dependency)
E fixture ‘user’ has a dependency cycle: user -> order -> user
原因与解决 :这是设计问题。比如 user fixture 依赖 order ,而 order 又依赖 user 。需要重新审视业务逻辑。通常的解决方法是创建更基础的 fixture。比如, order 不应该依赖一个具体的 user fixture,而应该依赖一个 user_factory fixture 和 user_data fixture(仅提供数据),然后在 order fixture 内部调用工厂创建用户。或者,将 user 和 order 拆分成两个独立的、不互相依赖的 fixture,在需要关联的用例中同时引入两者。
问题二:测试随机失败(Flaky Tests) 可能原因 :
- 数据冲突 :多个用例并行运行时,使用了相同标识(如用户名、手机号)的数据。 解决 :使用随机性更强的数据,如
faker+ 时间戳username = f”test_user_{fake.user_name()}_{int(time.time())}”。 - 状态残留 :上一个测试创建的数据未清理,影响下一个测试。 解决 :确保每个创建资源的 fixture 都有对应的清理逻辑(teardown)。使用
pytest的--tb=short和-v运行失败用例,观察 fixture 的 setup/teardown 日志。 - 异步操作未完成 :比如点了提交按钮后立刻去查数据库,可能数据还没落盘。 解决 :加入显式等待或轮询机制。
# 轮询查询,直到状态改变或超时 def wait_for_order_status(db_client, order_id, expected_status, timeout=10): start = time.time() while time.time() - start < timeout: status = db_client.query_order_status(order_id) if status == expected_status: return True time.sleep(0.5) raise TimeoutError(f“订单状态在{timeout}秒内未变为{expected_status}”)
问题三:测试数据过于复杂,工厂方法参数爆炸 当一个 create_order 方法需要传入几十个参数来控制优惠券、地址、发票等信息时,工厂就难用了。 解决 :使用 建造者模式(Builder Pattern) 或 选项对象(Options Object) 。
# 选项对象模式
class OrderOptions:
def __init__(self):
self.user = None
self.products = None
self.coupons = []
self.shipping_address = None
self.invoice_needed = False
# ... 可以加一些with_xxx的链式调用方法,让创建更流畅
class OrderFactory:
def create_order(self, options: OrderOptions):
# 根据 options 对象内的各种属性来构造请求
pass
# 在用例或夹具中
options = OrderOptions()
options.user = vip_user
options.products = [product1, product2]
options.coupons = [“FREESHIP”]
order = order_factory.create_order(options)
问题四:数据库断言繁琐 每个涉及状态变更的测试都要写一堆数据库查询和断言代码。 解决 :封装 “验证器(Validator)”夹具 。
@pytest.fixture
def db_validator(db_client):
class Validator:
@staticmethod
def order_should_be_paid(order_id):
order = db_client.query_order(order_id)
assert order is not None, “订单不存在”
assert order[“status”] == “PAID”, f“订单状态应为PAID,实际为{order[‘status’]}”
assert order[“paid_at”] is not None, “支付时间未记录”
return Validator()
# 在用例中使用
def test_pay_success(pending_order, api_client, db_validator):
# ... 执行支付操作
db_validator.order_should_be_paid(pending_order[“id”]) # 一行搞定所有数据库断言
从散乱的前置条件代码到高度组织化的测试数据工厂,这个转变带来的收益是长期的。它让自动化测试代码从“一次性脚本”变成了可维护、可扩展的“工程资产”。初期搭建确实需要投入,但当你看到新功能的测试用例能在几分钟内写完,当业务逻辑变更时只需要修改一两个工厂方法,所有关联用例就自动适配时,你会觉得这一切都是值得的。这套模式不仅适用于 API 测试,同样可以迁移到 UI 自动化(通过 Page Object 模型封装页面元素,Fixture 封装浏览器和用户会话)和单元测试(Fixture 封装 Mock 对象和测试数据)。关键在于建立起“分层”和“工厂化”的思维,这是提升测试工程化水平的必经之路。
更多推荐
所有评论(0)