测试数据准备一直是自动化测试里消耗时间最多的环节之一。一个下单功能可能依赖用户、商品、库存、优惠券等多个实体,测试人员如果不借助工具,只能靠手工一条条插入数据,既慢又容易产生脏数据。更麻烦的是,当业务规则变化时,这些分散在脚本里的造数逻辑需要逐个排查修改。解决这个问题的关键,是把造数职责从测试用例中剥离出来,交给专门的数据工厂统一处理,同时用模拟对象隔离外部依赖。

数据工厂的核心:把造数逻辑集中起来
很多测试脚本里都散落着重复的造数代码,比如创建用户时手动拼一个字典,创建订单时又复制一份用户数据。这样做的直接后果是:表结构一旦增加字段,所有相关测试都要改;而且不同测试生成的同名实体属性还不一致,排查问题时很难判断是数据问题还是代码问题。数据工厂的目标就是把这些规则收敛到一个类或一组函数中,对外只暴露简单的创建方法。
一个合格的数据工厂至少应该具备两个能力:默认值和参数覆盖。默认值保证工厂生成的实体可以直接使用,不必每次传入全部字段;参数覆盖则允许测试针对特定场景修改关键属性。下面是一个简单的 Python 数据工厂示例,它既可以把对象返回给内存使用,也可以在传入数据库会话时直接落库。
class UserFactory:
def __init__(self, db_session=None):
self.db_session = db_session
self.defaults = {
"name": "tester",
"age": 28,
"status": "active",
}
def create(self, **overrides):
data = self.defaults.copy()
data.update(overrides)
if self.db_session is not None:
user = User(**data)
self.db_session.add(user)
self.db_session.flush()
return user
return data
工厂的持久化策略需要根据测试类型区分。单元测试中通常不需要真实数据库,返回普通对象或字典即可;集成测试中则需要借助 db_session 把数据写入临时库,以便后续查询关联。这样的设计让工厂成为测试数据和数据库之间的唯一桥梁,后续维护字段时只需要改工厂内部的默认值,不会波及大量测试用例。
模拟对象:让外部依赖不再拖累测试
即便内部测试数据能通过工厂快速生成,外部依赖仍然可能成为测试失败的来源。支付网关、短信服务、消息队列这些模块往往不可控:要么需要真实网络,要么会消耗额度,要么响应速度很慢。模拟对象的作用就是用一个轻量替身来替代这些外部模块,让测试只关注当前业务逻辑是否正确,而不是外部服务是否在线。
模拟技术中经常被混淆的三个概念是 Stub、Fake 和 Mock。Stub 只返回预设结果,不记录任何行为;Fake 是可以工作的简化实现,比如内存版数据库;Mock 则既能返回结果,又能断言某个方法是否被调用、调用了几次。实际项目中,使用成熟的模拟框架可以省去手写替身的麻烦。下面这段代码用 Python 标准库里的 MagicMock 模拟了一个支付客户端。
from unittest.mock import MagicMock
class Cart:
total = 100
cart = Cart()
payment_client = MagicMock()
payment_client.charge.return_value = {"status": "success", "transaction_id": "tx_123"}
def checkout(cart, client):
result = client.charge(cart.total)
return result["status"]
assert checkout(cart, payment_client) == "success"
使用模拟对象时要守住一条边界:不要模拟自己系统内部的核心领域对象。比如下单服务里的商品库存计算,应该用真实对象加数据工厂生成的数据来验证;只有像支付这种跨系统边界的外部交互,才适合用 Mock 替代。如果到处模拟,测试确实跑得快,但一旦真实模块的行为与模拟不一致,测试就失去了保护价值。
数据工厂与模拟怎么配合:分层构造测试数据
数据工厂和模拟对象解决的是两个不同层面的问题。数据工厂负责准备系统内部依赖的数据,例如用户、商品、库存这些实体,它们必须满足数据库约束和业务状态;模拟对象负责隔离系统外部的交互,例如支付回调、物流接口。一个集成测试通常需要两者同时出现,否则要么内部数据准备困难,要么外部调用不稳定。
合理的分工是:先用数据工厂在数据库中创建真实的用户和购物车,再用 MagicMock 模拟支付网关,最后调用订单服务并断言订单状态。下面示例展示了这种配合方式。代码中的 db_session 可以由测试夹具提供,实际项目中通常与会话级别的临时库或事务回滚配合使用。
from unittest.mock import MagicMock
user = UserFactory(db_session).create(name="alice")
cart = CartFactory(db_session).create(user_id=user.id, total=200)
payment_client = MagicMock()
payment_client.charge.return_value = {"status": "success"}
order_service = OrderService(payment_client)
order = order_service.place_order(user.id, cart.id)
assert order.status == "paid"
这条分层思路还能帮助团队减少测试之间的耦合。当支付接口的测试账号不可用时,只需要调整模拟配置,不需要改动数据工厂;当数据库表结构变化时,只需要修正工厂,不需要重写模拟逻辑。测试代码因此变得更加稳定,也更易于定位问题。
避开常见坑:随机数据、清理与维护成本
有了数据工厂后,有些团队喜欢在默认值里使用随机数,认为这样可以构造出多样化的测试数据。这种做法看似聪明,实际很容易造成测试结果不稳定。例如用户名用了随机后缀,断言时却写死了某个固定值,就会偶发失败。更稳妥的方式是使用固定默认值,对于必须随机的字段单独传入,并在断言中基于该输入值进行计算。
测试数据清理是另一个容易被忽略的问题。如果数据工厂直接向共享数据库写入数据而不清理,时间一长会积累大量垃圾记录,甚至影响测试环境的性能。集成测试建议在用例执行后回滚事务,或者使用专门的临时表空间。对于无法回滚的场景,可以在工厂中增加按批次标记删除的能力,定期清理测试数据。
最后要警惕数据工厂本身的维护成本。工厂并不是越多越好,过度抽象会把简单问题复杂化。通常按照业务聚合根来划分工厂比较合理,例如 UserFactory、OrderFactory、ProductFactory,而不是为每个数据库表都建一个工厂。工厂内部可以适当使用关联创建,但要避免在造数阶段引入过深的对象图,否则后续维护会变得困难。