【免费下载链接】rewards-farmer
Automation for MS Rewards based on https://youtu.be/4qdPcMNaioA
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 tests3. 平台层: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 可以直接抄作业的四步:
- 拆分"页面解析"与"浏览器操作":把选择器解析逻辑(src/element_selectors.py)写成只依赖
find_element(s)的类,就能用 40 行 Fake 驱动喂任何页面形态; - 写一个最小 Fake 驱动:只实现被测代码真正调用的方法,
FakeDriver继承FakeElement,把浏览器窗口当根元素拼 DOM; - 真实浏览器测试一律加开关:
skipUnless+ 环境变量,默认套件保持零成本; - 每个修复都留下回归测试,并在 docstring 里记下 bug 的来龙去脉,让测试库成为团队的事故知识库。
想动手验证?克隆仓库后执行python -m unittest discover -s tests,几秒钟内跑完全部默认用例——这就是这套 Fake 驱动设计的最终回报:浏览器自动化项目,也能拥有秒级反馈的单元测试。
【免费下载链接】rewards-farmer
Automation for MS Rewards based on https://youtu.be/4qdPcMNaioA
相关推荐
Artillery测试自动化框架:关键字驱动与行为驱动设计
Artillery测试自动化框架:关键字驱动与行为驱动设计 你是否还在为复杂的API测试场景编写重复代码?是否在负载测试与功能验证之间来回切换工具?本文将带你探
性能测试接口测试CLIClean Architecture for Android数据层设计:Repository模式与DataSource分离实践
Clean Architecture for Android数据层设计:Repository模式与DataSource分离实践 想要构建一个可维护、可测试且高度
Android测试架构模式:android-testing项目中的领域驱动设计测试
Android测试架构模式:android testing项目中的领域驱动设计测试 你还在为Android测试代码混乱、维护困难而烦恼吗?本文将通过androi
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考