ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

开发可测试的Selenium自动化项目:rewards-farmer测试架构与Fake驱动设计分析

开发可测试的Selenium自动化项目:rewards-farmer测试架构与Fake驱动设计分析

【免费下载链接】rewards-farmer

Automation for MS Rewards based on https://youtu.be/4qdPcMNaioA

项目地址:https://gitcode.com/gh_mirrors/re/rewards-farmer
点击查看免费下载

rewards-farmer 是一个基于 Selenium 的 Microsoft Rewards 自动刷分脚本,用 Edge WebDriver 驱动浏览器自动完成每日任务。这个项目最值得学习的地方是它的测试架构:几乎全部单元测试都不启动真实浏览器,而是靠一套极简的 Fake 驱动和 mock 打桩完成。本文将从测试分层、Fake 驱动设计、典型测试手法三个角度,带你拆解这套可测试的 Selenium 自动化项目是如何搭建的。

项目结构与测试全景

先快速认识一下项目布局。源码在src/,测试在tests/,依赖由 Poetry 管理(见 pyproject.toml):

模块职责
src/browser.py启动 Edge WebDriver(start_driver)
src/element_selectors.py页面元素查找与解析(ElementSelectionUtils)
src/rewards_tasks.py六大任务的执行循环
src/main.py多账号运行入口(run_account / main)
tests/fakes.py全部测试共用的 Fake 驱动

运行方式只有一条命令:

python -m unittest discover -s tests

不需要 Selenium Server、不需要打开浏览器、不需要 Windows 环境。这是整个架构设计的出发点。

测试分三层:零成本、按需、按平台

rewards-farmer 把测试清晰地分成了三层,新手做 Selenium 项目时完全可以照抄这个思路:

1. 默认层:不需要浏览器的纯逻辑测试

占大多数。它们只测试"拿到页面内容后怎么解析、怎么决策",把浏览器换成 Fake 对象即可,例如 tests/test_element_selectors.py 和 tests/test_task_outcomes.py。

2. 按需层:真实浏览器测试用环境变量开关

少数测试确实要启动 Edge(比如验证两个账号的登录态互相独立)。它们用skipUnless挂在环境变量REWARDS_BROWSER_TESTS上,默认跳过:

@unittest.skipUnless( os.environ.get("REWARDS_BROWSER_TESTS"), "starts Edge twice; set REWARDS_BROWSER_TESTS=1 to run", )

参考 tests/test_multi_account.py#L288-L291。需要时显式开启:

REWARDS_BROWSER_TESTS=1 python -m unittest discover -s tests

3. 平台层:Windows 专属功能自动跳过

虚拟桌面功能依赖 Windows API,测试用@unittest.skipUnless(sys.platform.startswith("win"), ...)标注(见 tests/test_desktop_utils.py#L17),在 Linux/macOS 上直接跳过,不报错也不浪费时间。

💡 三层设计保证了unittest discover在任何 CI 环境里都能一键跑绿,而"贵"的测试只在需要时付出成本。

Fake 驱动设计:只实现"被调用的东西"

整套架构的核心是 tests/fakes.py——一个不到 40 行的文件。它模仿 Selenium 的WebElement和WebDriver,但只实现选择器代码真正会调用的方法:text、DOM 属性、可见性和find_element(s)。

FakeElement:可编程的假 DOM 节点

tests/fakes.py#L11-L34

class FakeElement: def __init__(self, text="", attributes=None, children=None, displayed=True): self.text = text self.attributes = attributes or {} self.children = children or {} # {(by, selector): [FakeElement, ...]} self.displayed = displayed

注意children的结构:一个{(定位方式, 选择器) -> 子节点列表}的字典。写测试时,相当于手工"拼"出一个最小 DOM 树,元素找不找得到、可见与否、文字是什么,全由测试自己决定。这比维护 HTML 快照文件更轻量,比 mock 返回值更贴近真实调用链。

FakeDriver:把"驱动"当成最大的"元素"

tests/fakes.py#L37-L42

class FakeDriver(FakeElement): """A driver behaves like an element for the lookups used here."""

FakeDriver直接继承FakeElement,因为 Selenium 的 WebDriver 本身支持find_element——从"按 id 找容器"这个视角看,浏览器窗口就是一个根元素。这一层继承省掉了重复代码,也让测试可以像搭积木一样构造页面:

status = FakeElement(text="Completed") card = FakeElement(children={(By.CSS_SELECTOR, STATUS_SELECTOR): [status]}) driver = FakeDriver(children={(By.ID, "moreactivities"): [card]})

设计要点:不伪装完整,只满足契约

fakes.py 的模块注释说得很直白:"Only what the selectors actually call is implemented"。它不实现click()、send_keys()、get()……因为被测的选择器代码根本不调它们。这带来两个好处:

  • 可读:40 行就能看懂全部行为;
  • 诚实:如果生产代码哪天开始调用 Fake 没实现的方法,测试会立刻AttributeError,提醒你契约变了。

典型测试手法拆解

手法一:用 Fake 区分"页面没这个区块"和"区块还没渲染完"

这是 tests/test_element_selectors.py 的主题。生产代码里,"元素不存在"(NoSuchElementException)和"元素在但还在渲染"(自定义异常 ElementNotReady)必须报不同的结论,否则慢页面会被误报成"此地区没有此功能"。Fake 驱动让两种状态各构造一次即可断言:

  • 可见副本有内容 → 返回卡片列表;
  • 只有隐藏的副本有内容(响应式布局会输出两份)→ 宁可大声失败,也不返回点不了的空链接(见 DuplicatedContainer);
  • 容器在但内容为空 → 抛ElementNotReady;
  • 容器彻底不在 → 抛NoSuchElementException。

四个用例把"缺失"和"未就绪"的边界钉死,全部靠几行FakeElement拼装完成。

手法二:录制型打桩,验证"鼠标真的落在目标上"

tests/test_mouse_trajectory.py 测试的是模拟鼠标轨迹的数学部分(贝塞尔曲线 + Fitts 定律)。它没有用mock,而是手写了两个小类:

  • RecordingActionBuilder:冒充 Selenium 的ActionBuilder,把每次move_to_location的坐标记录下来;
  • FakeMouseDriver:对execute_script里出现innerWidth的脚本返回[1920, 1080],其余返回None。

然后在setUp里把mouse_trajectory.ActionBuilder临时替换成录制版,tearDown里还原(见 L100-L105)。最后断言"最后一个坐标等于目标点"——这正是当年一个真实 bug 的回归测试:轨迹在 97.8% 处停止采样,指针每次都差几个像素,小按钮就点不中。

🎯 规律:"录制 + 替换 + 还原"三件套,是测试 Selenium 动作序列(ActionChains/ActionBuilder)最实用的手工打桩方式。

手法三:记录导航的 Fake Driver,验证失败后的页面恢复

tests/test_task_recovery.py 的 RecordingDriver 比 fakes.py 多记一件事:每次get(url)访问的 URL 和当前窗口句柄。测试场景是:搜索任务中途失败,浏览器停在 Bing 页面,后续任务全被误报跳过。现在代码会在任务失败后导航回 Rewards 主页,测试只需断言:

self.assertEqual(driver.visited, [HOME]) # 失败后恰好回了一次主页 self.assertEqual(driver.visited, []) # 干净的运行绝不多余导航

"干净运行零导航"这条反向断言很值得借鉴——它防止将来有人为"保险"而加无谓的页面刷新。

手法四:monkeypatch Selenium 类,测试失败隔离与启动报错

tests/test_multi_account.py 用"替换类"的手法验证多账号容错:把一个假Edge类和假Tasks类塞回 browser.webdriver 命名空间,让第二个账号在 7 种不同的失败位置(启动、连接、任务中途、退出……)依次抛出真实 Selenium 异常,然后断言其余账号照常跑完、已启动的浏览器一定被干净关闭(见 TestFailureIsolation)。

tests/test_edge_startup.py 同理:把webdriver.Edge换成会抛指定异常的函数,用 FAILURES 表 把"驱动原始报错 → 日志里必须出现的人话解释"逐条配对断言,连"未见过的新错误原样透传"都有用例。

手法五:mock 平台 API 与交互输入

  • Windows API:tests/test_desktop_utils.py 用mock.patch("desktop_utils.is_windows")翻转平台判断,再用mock.MagicMock冒充ctypes.windll.user32.keybd_event,甚至构造"按下成功、释放失败"的场景验证键位一定被释放(见 L277-L299)。
  • 交互配置:tests/test_init_config.py 测试带问答的配置脚本 src/init-config.py,做法是mock.patch("builtins.input", side_effect=["invalid", ""])预设一串"用户回答",再断言生成的.env文本。输入、重试、落盘全部不碰真实终端。

手法六:子进程 + 信号,验证优雅退出

tests/test_signin.py 验证容器内登录流程的退出安全:subprocess.Popen启动真实的signin.py,等SingletonLock出现后发SIGTERM,断言退出码为 0、锁文件消失、且能再次打开同一 profile。还有专门测试 TestLockOwnership:别的主机/别的进程留下的锁绝不能删,只有自己的锁才清理——用临时目录 + 符号链接就能模拟,依然不需要浏览器。

测试代码中的通用好习惯

习惯出处作用
setUp存环境变量、addCleanup还原test_multi_account.py#L61-L65测试顺序无关,互不污染
subTest遍历边界值test_desktop_utils.py#L41-L54一个用例覆盖"1/true/True/yes/ 空格"等全部变体
random.seed()固定随机数test_mouse_trajectory.py#L76轨迹含随机扰动也能稳定复现
assertLogs断言日志文案test_task_outcomes.py#L295-L303日志本身就是产品行为([SKIP]/[FAIL])
每个测试文件头部注明运行命令与背景 bug 号各test_*.pydocstring新人一眼看懂"为什么有这个测试"

最后一点尤其值得学:几乎每个测试类的 docstring 都写明了它对应的真实事故("#52:慢面板被误报成不可用"、"#74:指针差几个像素"、"#63:失败后后续任务全被跳过")。测试不是凭空设计的,而是每个线上 bug 的墓碑——这也是这套测试架构最有说服力的地方。

总结:如何给自己的 Selenium 项目搭测试

从 rewards-farmer 可以直接抄作业的四步:

  1. 拆分"页面解析"与"浏览器操作":把选择器解析逻辑(src/element_selectors.py)写成只依赖find_element(s)的类,就能用 40 行 Fake 驱动喂任何页面形态;
  2. 写一个最小 Fake 驱动:只实现被测代码真正调用的方法,FakeDriver继承FakeElement,把浏览器窗口当根元素拼 DOM;
  3. 真实浏览器测试一律加开关:skipUnless+ 环境变量,默认套件保持零成本;
  4. 每个修复都留下回归测试,并在 docstring 里记下 bug 的来龙去脉,让测试库成为团队的事故知识库。

想动手验证?克隆仓库后执行python -m unittest discover -s tests,几秒钟内跑完全部默认用例——这就是这套 Fake 驱动设计的最终回报:浏览器自动化项目,也能拥有秒级反馈的单元测试。

【免费下载链接】rewards-farmer

Automation for MS Rewards based on https://youtu.be/4qdPcMNaioA

项目地址:https://gitcode.com/gh_mirrors/re/rewards-farmer
点击查看免费下载

相关推荐

上一篇:Navi Widgets 完整指南:在 Bash、Zsh、Fish、Nushell、Tmux 与 Vim 中一键唤起 navi
下一篇:pyenv-installer 项目教程:Python 版本管理的终极解决方案

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进