ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

WeClaw_83|Mock、Fixture 与录制回放:三种测试替身术的工程实战

WeClaw_83|Mock、Fixture 与录制回放:三种测试替身术的工程实战 Hi带娃的我热爱AI 大模型应用落地、意识解码与 AI 开发工具链。 创业路上用技术换时间一起把 AI 变成生产力 WeClaw_83Mock、Fixture 与录制回放三种测试替身术的工程实战系列文章第 83 篇- 从 WeClaw v7.0.0 工作流队列 282 项测试中拆解 Mock / Fixture / 录制回放三种测试隔离策略的选型逻辑与实战代码 专栏信息《从零到一构建跨平台 AI 助手WeClaw 实战指南》专栏本文是模块九【工程实践与质量保障】第 1 篇。WeClaw v7.0.0 是一次架构级重大迭代——新增工作流队列核心域7 个模块、Agent 工具包、UI 组件包横跨持久化调度、安全模型、模板分享四个层面累计 282 项自动化测试全部通过。本篇以这次迭代的真实测试代码为案例讲解三种测试替身术的本质、选型与陷阱。‍ 作者与项目作者简介翁勇刚 WENG YONGGANG新概念龙虾-WeClaw 开发团队负责人一群专注于跨平台 AI 应用的实践者理念“再复杂的技术也能用代码讲清楚” 项目地址https://github.com/wyg5208/weclaw.git 官网地址https://weclaw.link 作者 CSDNhttps://blog.csdn.net/yweng18⭐ 欢迎 Star⭐、Fork、贡献代码 摘要核心问题单元测试要快、稳、独立但真实系统依赖数据库、网络、LLM、GUI 框架——如何在不调用任何外部服务的前提下验证业务逻辑三种替身术Mock模拟对象手写一个假的依赖你完全控制它的行为Fixture测试夹具预先搭好舞台——临时数据库、配置对象、初始数据录制回放Record Replay把一次真实交互录下来之后离线反复播放关键成果v7.0.0 迭代 282 项测试在无 PySide6/litellm 环境下全部通过连续复跑 3-15 次确认无 flaky过程中发现并修复 2 处测试自身竞态 2 处生产缺陷适合读者想系统理解测试隔离策略、正在为异步/IO 密集系统编写测试的 Python 开发者阅读时长约 18 分钟关键词Mock、Fixture、录制回放、pytest、asyncio测试、测试隔离、工作流队列一、为什么需要替身——从一个真实困境说起1.1 v7.0.0 的测试困境WeClaw v7.0.0 工作流队列的核心调度器SerialQueueDispatcher需要真实依赖为什么不能直接用GuiAgentLLM 对话调用一次 3-15 秒282 项测试跑完要 1 小时PySide6 GUI 框架CI 环境没有显示器QApplication直接崩溃litellm模型调用需要 API Key有速率限制结果不确定真实 SQLite 数据库文件测试间互相污染并发跑测试时文件锁冲突如果直接对接真实依赖测试会变成慢、不稳定、不可重复、需要外部环境。这违反了单元测试的四条基本原则——快Fast、独立Isolated、可重复Repeatable、自验证Self-validating。1.2 三种替身术的定位┌─────────────────────────────────────────────────────────┐ │ 测试隔离策略光谱 │ │ │ │ 完全虚构 ◄─────────────────────────────► 完全真实 │ │ │ │ Mock Fixture 录制回放 真实调用 │ │ (我编的) (我搭的舞台) (录下来的) (现场直播) │ │ │ │ 控制力高 ────────────────────────────── 低 │ │ 真实度低 ────────────────────────────── 高 │ │ 维护成本接口变 → 手动改 ──────── 接口变 → 重新录制 │ └─────────────────────────────────────────────────────────┘三者不是互斥的——一个成熟的测试套件往往同时使用三种。下面逐一拆解。二、Mock我假装是你2.1 本质Mock 的核心思想用一个完全受控的假对象替代真实依赖让测试只验证目标逻辑。你不需要真的调用 LLM只需要一个被调用时返回预设结果的替身。2.2 WeClaw 实战FakeExecutorv7.0.0 调度器测试中最核心的 Mock 是FakeExecutor——它替代了真实的GuiAgent.chat()# test_scripts/test_workflow_queue_dispatcher.pyclassFakeExecutor:可编程的假执行器按 instruction 内容决定行为记录执行顺序与并发峰值。def__init__(self,delay:float0.02):self.delaydelay self.order:list[str][]# 记录执行顺序self.concurrent0# 当前并发数self.max_concurrent0# 并发峰值验证串行性asyncdef__call__(self,item:WorkflowItem)-ExecutionOutcome:self.concurrent1self.max_concurrentmax(self.max_concurrent,self.concurrent)self.order.append(item.instruction)try:ifitem.instruction.startswith(SLOW_CANCEL):awaitasyncio.sleep(5)# 模拟长任务会被 request_stop() 取消returnExecutionOutcome(statusItemStatus.SUCCEEDED)awaitasyncio.sleep(self.delay)ifitem.instruction.startswith(FAIL):returnExecutionOutcome(statusItemStatus.FAILED,error模拟失败)returnExecutionOutcome(statusItemStatus.SUCCEEDED,result_summaryok)finally:self.concurrent-1设计精髓可编程行为通过指令前缀FAIL、SLOW_CANCEL控制返回成功/失败/超时一个类覆盖所有分支可观测状态order记录执行顺序max_concurrent验证严格串行——如果调度器有并发 Bugmax_concurrent 1立刻暴露零外部依赖不需要 LLM、不需要网络、不需要 GUI20ms 就能模拟一次执行2.3 验证串行性的断言asyncdef_test_strict_serial_and_advance():withtempfile.TemporaryDirectory(prefixweclaw_wfq_disp_)astmp:storageWorkflowQueueStorage(db_pathPath(tmp)/wfq.db)serviceWorkflowQueueService(storage)executorFakeExecutor(delay0.02)dispatcherSerialQueueDispatcher(service,executor)# 提交 5 条指令queueawaitservice.create_queue(测试,items[A,B,C,D,E])awaitdispatcher.start()await_wait_for_queue_status(service,queue.queue_id,{QueueStatus.COMPLETED})# 核心断言执行顺序 提交顺序且从未并发assertexecutor.order[A,B,C,D,E]assertexecutor.max_concurrent1,f检测到并发:{executor.max_concurrent}如果不用 Mock 而用真实 GuiAgent每次调用 3-15 秒 → 5 条指令至少 15 秒LLM 输出不确定 → 无法断言精确顺序需要 API Key → CI 跑不了2.4 unittest.mock 的标准用法在tests/test_binding_and_routing.py中使用了 Python 标准库的patchfromunittest.mockimportMagicMock,AsyncMock,patchpatch(remote_server.auth.user_manager.send_websocket_message)deftest_binding_notifies_only_target_device(mock_send):绑定成功后只通知目标设备不广播mock_send.return_valueNone# ... 执行绑定逻辑 ...# 验证只调用了一次且目标是正确的 device_idmock_send.assert_called_once()call_argsmock_send.call_argsassertcall_args[0][0]target_device_id2.5 Mock 的陷阱过度 Mock 导致测试通过但系统是坏的v7.0.0 迭代记录中有一条深刻的教训P0 的reorder_item()测试通过、接口调用成功但因为claim_next_item()从未读取它修改的字段实际执行顺序完全不受影响——这种测试通过但业务逻辑是假的情况只能靠端到端验证才能发现。这就是过度 Mock 的典型症状你 Mock 了太多东西测试验证的是Mock 的行为而不是系统的行为。WeClaw 的对策调度器测试用 FakeExecutorMock 执行层但 Storage/Service 层用真实 SQLite不 Mock——在可控与真实之间找到平衡点。三、Fixture我先把舞台搭好3.1 本质Fixture 的核心思想在每个测试运行前自动准备好它需要的环境和数据测试结束后自动清理干净。它不是假的——临时数据库是真实的数据库只是用完就扔。3.2 WeClaw 实战工厂函数式 Fixturev7.0.0 存储层测试没有使用 pytest 的fixture装饰器因为要支持无 pytest 直接运行而是用工厂函数实现等价语义# test_scripts/test_workflow_queue_storage.pydef_new_storage(tmp:Path)-WorkflowQueueStorage:每个测试拿到一个全新的、隔离的存储实例returnWorkflowQueueStorage(db_pathtmp/workflow_queue.db)def_make_queue(**overrides)-WorkflowQueue:队列工厂默认值 按需覆盖defaultsdict(queue_idnew_id(q),name测试队列,sourceQueueSource.DESKTOP_INTERACTIVE,session_iddesktop_main,)defaults.update(overrides)returnWorkflowQueue(**defaults)def_make_item(queue_id:str,sequence:int,instruction:str指令,**overrides)-WorkflowItem:项目工厂最小必填参数 按需扩展defaultsdict(item_idnew_id(i),queue_idqueue_id,sequencesequence,instructioninstruction,)defaults.update(overrides)returnWorkflowItem(**defaults)每个测试函数内部用tempfile.TemporaryDirectory确保隔离deftest_schema_and_crud():withtempfile.TemporaryDirectory(prefixweclaw_wfq_)astmp:storage_new_storage(Path(tmp))# 全新数据库queue_make_queue(name我的队列)# 全新队列对象storage.create_queue(queue)fetchedstorage.get_queue(queue.queue_id)assertfetched.name我的队列# with 块结束 → 临时目录自动删除 → 零残留3.3 pytest 原生 Fixture 对比在tests/test_binding_and_routing.py中使用了 pytest 内置的tmp_pathfixtureclassTestDeviceFingerprintUniqueConstraint:deftest_same_fingerprint_cannot_bind_two_users(self,tmp_path):同一设备指纹不能同时绑定两个不同用户ummake_user_manager(tmp_path/test.db)# pytest 自动提供临时路径user1um.create_user(user1,password123)user2um.create_user(user2,password456)# ...tmp_path是 pytest 内置 fixture——每个测试函数自动获得一个唯一的临时目录测试结束后由 pytest 统一清理。3.4 全局 Fixtureconftest.py# conftest.py项目根目录pytest 全局配置 - 设置测试路径importsysfrompathlibimportPath WECLAW_SERVER_DIRstr(Path(__file__).parent/weclaw_server)ifWECLAW_SERVER_DIRnotinsys.path:sys.path.insert(0,WECLAW_SERVER_DIR)这个隐形 Fixture确保所有测试文件都能from remote_server.auth.user_manager import UserManager无需每个文件重复写路径注入。3.5 Fixture 设计原则WeClaw 实践总结原则说明WeClaw 示例最小化只提供测试真正需要的_make_item()只有 4 个必填字段可覆盖默认值 **overrides模式_make_queue(statusQueueStatus.PAUSED)自清理不留残留测试间零耦合TemporaryDirectory上下文管理器层次化全局 → 模块 → 函数三级conftest.py→ 文件头 helper → 测试内四、录制回放我把你的表演录下来4.1 本质录制回放的核心思想第一次运行时把真实交互请求-响应对保存到文件之后每次运行直接从文件读取不再发起真实调用。与 Mock 的区别Mock 的返回值是你编的录制回放的返回值是真实系统曾经返回的。4.2 WeClaw 的准录制回放实践v7.0.0 安全策略测试采用了一种准录制回放策略——直接使用仓库中的真实配置文件作为测试输入# test_scripts/test_workflow_queue_security.py 不依赖 PySide6/litellmconfig/tools.json 使用仓库真实文件验证与实际配置 的一致性而不是构造假配置绕过真实判定逻辑。 deftest_is_action_safe_for_unattended_real_config():# 直接读取仓库中真实的 config/tools.jsoncheck(低风险工具datetime_tool无需预授权,wq_security.is_action_safe_for_unattended(datetime_tool,get_current_time),)check(高风险 shell.run 不在 safe_actions 中需要预授权,notwq_security.is_action_safe_for_unattended(shell,run),)check(未知工具保守返回 False不视为安全,notwq_security.is_action_safe_for_unattended(__not_a_real_tool__,anything),)为什么不用 Mock 构造一个假的 tools.json迭代文档给出了明确理由验证与实际配置的一致性而不是构造假配置绕过真实判定逻辑。如果你 Mock 了tools.json测试验证的是你假设的配置长什么样用真实文件验证的是系统在当前真实配置下是否安全。配置改了测试立刻能发现。4.3 标准录制回放工具VCR.py 示例对于 HTTP API 测试Python 生态有成熟的录制回放库# 以 vcrpy 为例WeClaw 远程桥接测试的推荐方案importvcrimportrequestsvcr.use_cassette(tests/cassettes/weather_api.yaml)deftest_weather_query():# 第一次运行真实请求 → 录制到 yaml# 之后运行直接从 yaml 读取响应零网络resprequests.get(https://api.weather.com/v1/current?citybeijing)assertresp.status_code200asserttemperatureinresp.json()录制后的weather_api.yaml长这样interactions:-request:body:nullheaders:{}method:GETuri:https://api.weather.com/v1/current?citybeijingresponse:body:string:{temperature: 28, humidity: 65, city: beijing}headers:content-type:application/jsonstatus:code:200message:OKversion:14.4 录制回放的适用场景场景为什么适合录制回放第三方 API天气、股票、翻译有速率限制、收费、结果随时间变化LLM 响应不确定性高、成本高、速度慢OAuth 认证流程不可能每次测试都真的走一遍授权WebSocket 长连接交互建连成本高、状态复杂4.5 录制回放的陷阱陷阱说明对策数据过期录制的响应可能已不符合新 API 版本定期重新录制 CI 中加新鲜度检查敏感信息泄漏响应中可能包含 Token/用户数据录制后脱敏vcrpy 支持filter_headers过度依赖快照只验证和上次一样不验证是否正确录制回放 关键断言结合使用五、三者的协同v7.0.0 测试架构全景5.1 分层策略┌─────────────────────────────────────────────────────────────┐ │ test_workflow_queue_dispatcher.py (73 项) │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Mock 层FakeExecutor 替代 GuiAgent │ │ │ │ Fixture 层TemporaryDirectory Service 工厂 │ │ │ │ 真实层WorkflowQueueStorage真实 SQLite │ │ │ └─────────────────────────────────────────────────────┘ │ ├─────────────────────────────────────────────────────────────┤ │ test_workflow_queue_storage.py (87 项) │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Fixture 层_new_storage / _make_queue / _make_item │ │ │ │ 真实层100% 真实 SQLite 操作零 Mock │ │ │ └─────────────────────────────────────────────────────┘ │ ├─────────────────────────────────────────────────────────────┤ │ test_workflow_queue_security.py (35 项) │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 录制回放层真实 config/tools.json 作为测试输入 │ │ │ │ 真实层security.py 的完整判定逻辑零 Mock │ │ │ └─────────────────────────────────────────────────────┘ │ ├─────────────────────────────────────────────────────────────┤ │ test_workflow_queue_tool.py (33 项) │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Fixture 层_make_tool_with_service 工厂 │ │ │ │ 真实层ToolRegistry Service Storage 全真实 │ │ │ │ 明确注释直接使用真实 WorkflowQueueStorage不 mock│ │ │ └─────────────────────────────────────────────────────┘ │ ├─────────────────────────────────────────────────────────────┤ │ test_workflow_queue_templates.py (54 项) │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 录制回放层3 个内置示例模板 JSON/CSV 作为基准数据 │ │ │ │ Fixture 层临时目录 构造的模板字符串 │ │ │ └─────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘5.2 选型决策树需要测试的依赖是什么 │ ├─ 纯计算逻辑无 IO │ └─ 不需要替身直接测 │ ├─ 数据库 / 文件系统 │ └─ Fixture临时目录 真实引擎不 Mock SQLite │ 理由SQLite 本身够快内存级Mock 它反而丢失真实性 │ ├─ 外部服务LLM / HTTP API / WebSocket │ ├─ 只关心是否被调用、参数对不对 → Mock │ ├─ 关心返回值处理逻辑 → 录制回放 │ └─ 关心端到端正确性 → 集成测试少量慢速手动触发 │ ├─ GUI 框架PySide6 │ └─ Mock 或跳过pytest.mark.skipif(not HAS_PYSIDE6) │ └─ 配置文件tools.json / default.toml └─ 录制回放思路直接用仓库真实文件不构造假配置六、异步测试中的竞态陷阱——来自 v7.0.0 的血泪教训6.1 问题固定 sleep 断言v7.0.0 迭代中发现的两处测试竞态本质是Fixture 的时间维度设计失误# ❌ 错误写法固定延时后断言瞬时状态awaitservice.create_workflow(test,items[A])awaitasyncio.sleep(0.05)# 猜调度器已经开始了assertexecutor.order[A]# 偶发失败create_workflow内部会唤醒 dispatcher 协程但唤醒到实际执行之间有调度延迟。0.05 秒在大多数时候够用但在 CI 高负载时不够——这就是 flaky test 的根源。6.2 对策条件轮询等待# ✅ 正确写法轮询等待目标状态asyncdef_wait_until(predicate,timeout:float5.0,interval:float0.02)-bool:loopasyncio.get_event_loop()deadlineloop.time()timeoutwhileloop.time()deadline:ifpredicate():returnTrueawaitasyncio.sleep(interval)returnpredicate()# 使用awaitservice.create_workflow(test,items[A])await_wait_until(lambda:Ainexecutor.order,timeout5.0)assertexecutor.order[A]原则给异步系统写测试时永远不要猜时机要等状态。6.3 更可靠的等待轮询队列状态而非执行器记录asyncdef_wait_for_queue_status(service,queue_id,statuses,timeout5.0):轮询队列真实状态比轮询 executor.order 更可靠 order 在执行器调用*开始*时即写入不代表 finalize_item 已完成loopasyncio.get_event_loop()deadlineloop.time()timeoutwhileloop.time()deadline:qawaitservice.get_queue(queue_id)ifqandq.statusinstatuses:returnq.statusawaitasyncio.sleep(0.03)returnNone这段注释揭示了一个微妙问题executor.order.append()发生在执行开始时但队列状态迁移发生在执行结束后。如果你要断言队列已完成必须等队列状态而不是执行器记录。七、三者的对比总结维度MockFixture录制回放本质手写假对象预置环境/数据保存真实交互真实度低你编的中数据真、环境临时高真实响应速度极快ns 级快ms 级SQLite 建表快读文件维护成本接口变 → 手动改低自动清理接口变 → 重新录制典型工具unittest.mock/FakeXxxpytest.fixture/tmp_pathvcrpy/responsesWeClaw 用例FakeExecutor 替代 GuiAgentTemporaryDirectory 工厂函数真实 tools.json 作为输入最大风险过度 Mock → 测了个寂寞搭建成本过高 → 测试变慢数据过期 → 假绿八、实战建议何时用什么8.1 优先用 Fixture 的情况依赖本身够快、够轻SQLite、内存缓存、本地文件你需要验证真实的 IO 行为SQL 语法、事务隔离、文件编码WeClaw 的选择Storage 层 87 项测试零 Mock全用真实 SQLite8.2 必须用 Mock 的情况依赖慢、贵、不确定LLM 调用、第三方 API你需要验证交互行为是否调用了、参数对不对、调用了几次依赖不可用CI 没有显示器 → Mock GUIWeClaw 的选择FakeExecutor 替代 3-15 秒的 LLM 调用8.3 适合录制回放的情况外部 API有速率限制/收费响应内容复杂但相对稳定你需要离线可重复的集成测试WeClaw 的选择安全测试直接读仓库真实配置“穷人版录制回放”8.4 黄金法则能真实就真实必须隔离才替身替身时只替不可控的那一层。v7.0.0 的 282 项测试之所以能在无 PySide6/litellm 环境下全部通过且无 flaky核心原因不是Mock 用得多而是精确选择了在哪一层切断真实依赖执行层GuiAgent/LLM→ MockFakeExecutor存储层SQLite→ 真实Fixture 提供临时实例配置层tools.json→ 真实录制回放思路九、结语测试替身术不是造假而是控制变量。科学家做实验时不会把整个自然界搬进实验室——他们控制温度、压力、浓度只让一个变量变化。Mock 控制谁被调用Fixture 控制环境长什么样录制回放控制外部世界返回什么。三者协同让你在 0.2 秒内跑完 87 项存储测试在 20ms 内模拟一次 LLM 对话在离线环境中验证安全策略与真实配置的一致性。v7.0.0 迭代记录的最后一条经验教训说得好编码完成与经过验证之间的差距只能靠认真测试来弥补。而认真测试的前提是选对替身。下一篇预告WeClaw_84故障注入测试如何在实验室里制造崩溃
返回列表