十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

C++工程师视角:Selenium Web自动化测试从概念到实战指南

C++工程师视角:Selenium Web自动化测试从概念到实战指南 开头先说实话如果你是 C 背景的工程师第一次接触 Web 自动化测试大概率会经历一次不小的心理落差——打开 Selenium 官网翻到语言绑定列表看到里面整齐排列着 Java、Python、C#、Ruby、JavaScript甚至 Kotlin就是你最熟悉的 C 不在里面。我当时第一反应是那我这几年积累的 C 经验岂不是白学了后面想明白了这事没那么简单也没那么悲观。C 背景不仅不是劣势反而在你理解 WebDriver 的底层机制、处理复杂测试场景、设计高复用测试框架时给你带来纯脚本语言工程师不具备的视角。这篇文章我就从 C 工程师的视角出发把 Web 自动化测试从概念到 Selenium 实战系统地捋一遍重点讲清楚那些文档不会写、但实际动手时一定会遇到的关键选择。1. Selenium 官方不支持 C但 WebDriver 协议替我们留了一扇门1.1 先搞清楚 Selenium 和 WebDriver 到底是不是一回事很多人一上来就陷入用 C 调 Selenium的死胡同其实是把 Selenium 和 WebDriver 混为一谈了。Selenium 是一个项目集合它包含 Selenium IDE录制回放工具、Selenium WebDriver自动化控制浏览器的核心 API、Selenium Grid分布式执行工具。我们平时说的用 Selenium 做自动化99% 的场景指的就是 WebDriver。WebDriver 的本质不是一门语言而是一套协议。这套协议定义了浏览器如何对外暴露自动化控制能力启动会话、打开 URL、查找元素、点击、输入文本、截图、执行 JavaScript全部都是标准化的 HTTP 接口。简单说你把浏览器启动起来它就变成一个 HTTP 服务器你的测试脚本就是客户端两边通过 JSON 格式的消息通信。ChromeDriver、GeckoDriver、EdgeDriver 这些驱动就是浏览器和测试脚本之间的翻译官。这里的关键点来了既然通信方式是标准的 HTTP JSON那么理论上任何编程语言只要能发起 HTTP 请求、能解析 JSON都能做 Web 自动化。Selenium 官方提供 Python、Java 等语言的绑定本质上是帮你封装好了协议细节让你不用手写 HTTP 请求。C 没有官方绑定只是说明官方团队没有投入资源去维护一套 C 版本的封装库不代表这条路走不通。1.2 如果一定要用纯 C 做 Web 自动化有三条现实路径你可以用 libcurl 或 cpprestsdk 这类 C HTTP 库手动向 ChromeDriver 发送符合 W3C WebDriver 标准的请求。比如启动一个会话请求大概是这样的// 伪代码向 ChromeDriver 的 9515 端口发送 POST /session 请求 std::string body R({ capabilities: { alwaysMatch: { browserName: chrome } } }); auto response http_client.post(http://localhost:9515/session, body); // 从 response 里解析 sessionId后续所有操作都要带上这个 id这条路径完全可行但工程量不小。你要自己封装会话管理、元素定位、等待策略、异常处理等于把 Selenium 官方帮你做的那层封装重新写一遍。如果你只是想把 Web 自动化集成到一个已有的 C 大型项目里又没有权限引入 Python 运行时这条路值得考虑。但作为入门学习我不建议一上来就这么干。另一条路径是使用社区维护的第三方 C Selenium 库比如早期的 selenium-cpp 项目。我亲自试用过最大的问题是维护滞后很多库停留在只支持旧版 WebDriver 协议的状态遇到新版 Chrome 的 65 以上版本就各种水土不服。做技术选型时你要有心理准备这类库可能连 issue 都没人回。第三条路径也是我最推荐的入门方式用 Python 写自动化脚本但是用 C 的工程素养去组织代码。这听起来像是背叛但实际工作中这是效率最高的方案。Python 语法简洁Selenium 官方绑定完善社区资料海量遇到问题搜一下基本都有答案。你真正要迁移的不是语言而是 C 给你的那一套严谨的思维方式。1.3 C 工程师学自动化测试到底比脚本工程师强在哪我后来想明白了一个道理写自动化测试本质上是写一段程序去控制另一段程序。C 工程师在这件事上有几个天然优势。第一你理解资源这个概念。浏览器进程、WebDriver 服务、页面元素在 C 工程师眼里全是需要管理生命周期的资源天然就会想到 RAII、想到异常安全、想到析构函数里要释放。而很多脚本工程师写自动化driver 开了不知道关测试跑多了内存泄漏、进程残留就是因为你没受过这种训练。第二你理解指针。后面我会详细讲元素定位和指针操作极其相似定位一个不存在的元素就像解引用一个空指针页面刷新导致元素失效就像指针指向的内存被释放了。这种类比能让你少踩很多坑。第三你理解编译原理和运行时。浏览器执行 JavaScript、页面异步渲染、网络请求时序这些对你来说不是黑盒。遇到动态内容加载这样的问题你会本能地从事件循环阻塞与非阻塞的角度去分析而不是盲目乱试。2. 从 C 到 Python核心是思想迁移不是重学一门语言2.1 先给一份快速对照表五秒钟完成心理建设很多 C 程序员抵触 Python是因为觉得这门语言不严谨——变量不用声明类型、缩进即代码块、运行时才发现错误。这些确实是不适应的地方但不代表你前面十几年 C 白学了。语法层面的差异很小你真正需要迁移的是这一套对应关系C 概念Python 对应物说明类和对象class/实例语法更简洁没有访问权限修饰符全靠约定指针对象引用Python 是引用语义赋值不拷贝类似 shared_ptr 的行为STL 容器list/dict/setdict 对应 maplist 对应 vector但用起来灵活得多智能指针垃圾回收不用手动 delete但有资源仍需显式释放异常 try/catchtry/except思路一致Selenium 异常体系很完善编译期错误运行期错误这确实是个痛点所以我强烈建议用 IDE 加类型标注我自己刚转的时候最难受的是没有编译器帮我提前发现拼写错误。解决方案很简单装一个 Pylance 扩展给关键函数加类型注解让 IDE 在一定程度上替你进行静态检查。这样既保留了 Python 的开发效率又不至于让低级错误拖慢节奏。2.2 环境准备VS Code 配置和 pip 换源既然你是 C 背景VS Code 大概率已经在用了。我建议直接复用 VS Code 作为 Python 开发环境不用额外装 PyCharm。需要装的东西就三个Python 解释器、Pylance 扩展、Python 扩展。打开一个文件夹新建 hello.py写一行业务逻辑按 F5 能跑通环境就 OK 了。然后是安装 Selenium 库。如果你在国内网络环境强烈建议先把 pip 源换成清华或阿里云的镜像否则下载速度会让你怀疑人生。换源的方式是在用户目录下创建 pip.ini 文件写入[global] index-url https://pypi.tuna.tsinghua.edu.cn/simple trusted-host pypi.tuna.tsinghua.edu.cn然后执行pip install selenium顺手装一个 pytest 和 webdriver-manager前者用来组织测试用例后者用来简化浏览器驱动的管理。后面你会发现这两个库能让你的自动化代码从能跑进化到能维护。2.3 用 C 程序员的方式调试 PythonPython 调试和 C 调试有个巨大的差异C 里你习惯在编译期尽可能多地捕捉错误Python 则明显更依赖运行时反馈。刚开始写 Selenium 脚本时我经常遇到AttributeError: NoneType object has no attribute click这种报错第一反应是这代码在 C 里根本编译不过。解决办法其实很简单在关键位置打印调试信息用小步快跑的方式确认每一步的中间结果。比如定位元素之前先打印页面的标题、当前 URL、页面源码片段确认脚本确实导航到了你预期的页面。这种打桩式调试虽然看起来原始但在自动化测试脚本里反而非常实用。你不需要写复杂的单元测试因为脚本本身就是在和真实环境交互。找个打印按钮能让你在十分钟内定位问题出在定位、等待还是操作上。3. 环境搭建ChromeDriver 版本匹配是绝大多数新手的第一道坎3.1 驱动版本匹配的错误做法和正确做法这是我在无数新手提问帖里看到最多的问题也是热搜词里web自动化selenium浏览器驱动怎么判断下载哪个区别这句搜索背后真正的痛点。很多教程告诉你去 ChromeDriver 下载页面下载一个就行但实际不是这样版本对不上启动浏览器时就会报SessionNotCreatedException: Message: session not created: This version of ChromeDriver only supports Chrome version 114我先说正确流程再说判断逻辑。第一步打开你的 Chrome 浏览器在地址栏输入chrome://version记下第一行显示的版本号比如131.0.6778.86。第二步去 ChromeDriver 官方下载页面找到和你的 Chrome 主版本号一致的目录Chrome 131 就找 131.x.x.x 的版本。第三步下载和你操作系统对应的压缩包Windows 就下载 chromedriver-win32.zip解压得到一个 chromedriver.exe。这个版本对应的逻辑要说清楚。Chrome 和 ChromeDriver 遵循相同的版本号规则一个四位版本号主版本.次版本.修订号.构建号。ChromeDriver 的大版本号必须和 Chrome 的大版本号一致也就是首位数字相同。后面的三位不必完全一致但尽量选择最新的子版本因为官方会在子版本中修复一些 BUG。如果你用的是 Chrome for Testing 这个专门用于自动化的浏览器版本和 ChromeDriver 的匹配会更严格需要完全相同。3.2 三个驱动管理方案从手工到自动手工管理驱动有一个烦人的问题Chrome 每隔几周就自动升级一次升级之后你的驱动可能就失效了测试脚本莫名报错。为了根治这个问题我给你三个递进的方案。方案一直接把 chromedriver.exe 放到一个固定目录下在脚本里手动指定路径from selenium import webdriver driver webdriver.Chrome(executable_pathrD:\tools\chromedriver.exe)这个方案简单直接但 Chrome 一升级就崩需要手动重新下载覆盖。方案二把 chromedriver.exe 所在的目录加入系统 PATH 环境变量。这样代码里不需要再写 executable_pathSelenium 会自动去 PATH 里找可执行文件。省去每次修改路径的麻烦。方案三用webdriver-manager这个库让它自动检测当前浏览器版本并下载匹配的驱动from selenium import webdriver from webdriver_manager.chrome import ChromeDriverManager driver webdriver.Chrome(serviceService(ChromeDriverManager().install()))第一次运行会自动下载驱动并缓存之后 Chrome 升级了也能自动适配。这是我现在最推荐的方式一劳永逸。网上搜selenium安装 驱动下载的大多数问题用这个方案直接就不存在了。3.3 第一个能跑起来的脚本以百度搜索为最小闭环环境配置好之后我建议你先跑通一个最简脚本不求复杂只求验证整个链路通了。目标很简单打开百度首页在搜索框输入Selenium点击搜索按钮打印搜索结果标题。from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC driver webdriver.Chrome() # 如果配置正确这里会弹出浏览器窗口 try: driver.get(https://www.baidu.com) # 等待搜索框出现最长等 10 秒 search_box WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, kw)) ) search_box.send_keys(Selenium) # 点击搜索按钮 driver.find_element(By.ID, su).click() # 等待搜索结果加载打印第一条结果的标题 WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.XPATH, //div[idcontent_left]//h3)) ) titles driver.find_elements(By.XPATH, //div[idcontent_left]//h3) print(titles[0].text) finally: driver.quit()这段代码注意一个细节driver.quit()被放在finally里。这就是 C 工程师的本能——你绝不会允许自己写程序不释放资源。在 Python 自动化测试里driver.quit()就是析构函数它负责关闭浏览器进程、释放 WebDriver 会话。如果漏写内存里会残留一堆 chromedriver 进程跑十次脚本你系统里就有十个僵尸浏览器。4. 元素定位实战把 DOM 节点当成 C 指针来理解4.1 为什么元素定位像指针操作——用这个类比少走弯路如果你把网页的 DOM 树想象成一片堆内存每个 HTML 元素就是一块分配出来的对象那么find_element就是取地址element.click()就是解引用调用成员函数。有了这个类比很多概念瞬间清晰了。第一定位不到元素就像解引用空指针。你写driver.find_element(By.ID, nonexistent),找不到元素时抛出的NoSuchElementException本质上就是segmentation fault的优雅版。但报错的真正原因往往不是元素不存在而是时机不对——你以为页面加载完实际根本没加载。第二元素对象失效就像悬垂指针。定位到一个元素对象并保存下来然后页面发生了局部刷新之前那个 DOM 节点被浏览器销毁重建这时你再拿旧的元素对象去点击就会抛出StaleElementReferenceException本质上是指针指向了一块已释放的内存。第三元素集合和querySelectorAll的关系就像容器和迭代器。find_elements返回的是一个列表你要先判断列表是否为空再做下标访问否则IndexError和越界访问是一回事。有了这个类比你学习时的心理负担会小很多也更容易理解下面这些坑为什么存在。4.2 根据菜单名定位元素的三种可靠方式很多小伙伴做 Web 自动化时遇到最多的场景是页面上有一个菜单栏里面有用户管理订单管理系统设置等菜单项我想根据文字内容点某个菜单。这部分正好是热搜词里selenium根据菜单名定位元素对应的问题我直接给出三种可靠方案从优先到备选排列。首选方式是By.LINK_TEXT针对超链接菜单最直接menu_item driver.find_element(By.LINK_TEXT, 用户管理) menu_item.click()但如果菜单不是纯文本链接而是带有图标、嵌套 span 的复杂结构LINK_TEXT可能匹配不到。这时用By.XPATH通过文本内容定位任意元素# 匹配所有节点中文本恰好等于用户管理的元素且要求它是可见的 menu_item driver.find_element( By.XPATH, //*[text()用户管理 and not(ancestor::*[styledisplay: none])] ) menu_item.click()XPath 的//*是查找所有节点text()是判断文本内容。需要注意text()匹配的是元素的直接文本如果用户管理这四个字嵌在子元素里比如spani/i用户管理/span这时用contains(text(), 用户管理)来匹配。第三种方式是By.CSS_SELECTOR配合层级关系。如果菜单项有明确的 class 或 data 属性这种方式最稳定# 根据 class 和文本属性组合定位 menu_item driver.find_element( By.CSS_SELECTOR, li.nav-item[data-menuuser] )我给这几种方式排优先级是有原因的XPath 最灵活但性能较差能用 CSS 选择器解决的就不要升格成 XPathLINK_TEXT最直观但受限于必须是a标签。4.3 显式等待和隐式等待其实是自旋锁和 sleep 的取舍定位元素这一节绕不开等待机制。刚开始写自动化的人最容易犯的错就是页面刚发起了跳转脚本立刻去找新页面里的元素结果找不到。有些老教程让你用time.sleep(5)硬等这在 C 里就相当于写死一个延时粗暴且不稳定。Selenium 提供了两种规范的等待方式。隐式等待是全局设置告诉 WebDriver每次查找元素时如果没找到最多继续等这么久类似设了一个超时时间driver.implicitly_wait(10) # 设置 10 秒隐式等待整个 session 生效显式等待则是对特定条件的精确等待更接近条件变量 锁的语义element WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.ID, submit-btn)) )如果你把隐式等待类比成每个查找操作都自带一个超时时钟那显式等待就是条件不满足就阻塞满足条件才继续执行。显式等待支持很多条件element_to_be_clickable、presence_of_element_located、visibility_of_element_located是最常用的三个。实际项目中我建议只用显式等待因为隐式等待设了全局超时和显式等待混用时某些驱动会出现超时时间被意外叠加的问题这是个很难排查的隐坑。5. 测试代码的工程化用 C 的封装与 RAII 思想组织自动化脚本5.1 POM 模式把你的 pytest 脚本变成可维护的类写自动化脚本写到第三四天你一定会遇到代码爆炸的问题一个测试文件几百行全是重复的find_element和send_keys看一眼就想重构。这时候你多年的 C 类设计经验派上了用场。业界解决这个问题的标准答案是 POMPage Object Model页面对象模型核心思想和 C 里的封装完全一致——把页面抽象成一个类把页面上的操作封装成方法测试用例只关心调用这个页面的什么功能不关心元素长什么样。比如你做一个登录页面的测试设计一个 LoginPage 类from selenium.webdriver.common.by import By class LoginPage: def __init__(self, driver): self.driver driver # 把定位器作为常量维护在类里改页面结构时只需要改这里 self.username_input (By.ID, username) self.password_input (By.ID, password) self.login_button (By.CSS_SELECTOR, button.submit) def load(self): self.driver.get(https://example.com/login) def login(self, username, password): self.driver.find_element(*self.username_input).send_keys(username) self.driver.find_element(*self.password_input).send_keys(password) self.driver.find_element(*self.login_button).click()然后在测试用例里你的代码会变得非常干净def test_login_success(): driver webdriver.Chrome() login_page LoginPage(driver) login_page.load() login_page.login(tester, 123456) assert 欢迎 in driver.page_source driver.quit()这一层抽象的价值类比到 C 就是通过接口调用不直接操作成员变量。页面改了元素名或结构你只需要修改 LoginPage 这一个类所有测试用例都跟着变。如果你的同事还在写一个测试里嵌套八层 find_element的脚本你可以把这段代码给他看看对比立竿见影。5.2 测试失败时不留下任何线索等于没写测试一个测试运行失败你第一件事是干什么是打开浏览器复现还是翻日志如果脚本什么信息都没留你只能在本地一遍遍手动复现效率极低。所以我在每个测试项目里都坚持做两件事失败截图和日志记录。在 pytest 里可以做一个 fixture 来统一处理import pytest from datetime import datetime pytest.fixture def driver_with_screenshot(): driver webdriver.Chrome() yield driver if hasattr(pytest, current_test_failed) and pytest.current_test_failed: timestamp datetime.now().strftime(%Y%m%d_%H%M%S) driver.save_screenshot(fscreenshots/failure_{timestamp}.png) driver.quit()你看yield前后的代码就相当于 RAII 的构造和析构setup 和 teardown 全部集中在一个地方。测试失败自动保存现场排查问题的时候打开截图一眼就能看出是页面没加载出来、弹窗挡住了元素还是元素被改没了。这比对着几百行控制台输出猜原因高效太多。5.3 一行命令跑完所有测试再用 HTML 报告展示结果手工测试的痛点是不能回归自动化测试如果跑起来很麻烦也会被团队慢慢弃用。我建议用 pytest 的插件体系把测试组织成可执行的命令。安装这两个插件pip install pytest-html然后在项目根目录运行pytest -v --htmlreport.html --self-contained-htmlpytest 会自动发现以test_开头或以_test结尾的 Python 文件执行内部的测试函数生成一份带测试用例状态、失败原因、耗时信息的 HTML 报告。凡是 CI 能跑的东西这里都能接上。代码入库之前一条命令就能把核心回归全跑一遍这对团队的意义等你经历过一次上线前手动回归三小时的痛苦就懂了。6. 一些真实项目里的细节文档里搜不到的坑和对应的解法6.1 元素定位不到时先别优化定位器先确认页面状态这是我在实际项目里踩过最深的坑。为了定位一个按钮我把 XPath 从//button[text()提交]改到//div[classform]/div[2]/button越写越长、越写越脆弱结果最后发现问题根本不是定位器的问题——是页面有一个遮罩层挡住了按钮虽然它在 DOM 里存在但点击时被拦截了。Selenium 在 WebDriver 协议里规定click()是真实用户点击的模拟如果元素的中心点被其他元素遮挡点击会失败或者点到遮挡层上。遇到这种情况你首先应该用下面的代码检查元素状态element driver.find_element(By.XPATH, //button[text()提交]) print(element.is_displayed()) # 是否可见 print(element.is_enabled()) # 是否可点击 print(element.location) # 元素坐标如果is_displayed()返回 True 但is_enabled()是 False说明按钮是灰的这时应该检查前置条件是否满足。如果元素不在视口内可以用element.location_once_scrolled_into_view把元素滚动到可视区域。顺序一定是先确认状态、再解决状态、最后优化定位器别本末倒置。6.2 动态内容加载等待元素出现不等于等待数据就绪另一个高频问题是处理异步加载的数据列表。你等一个表格出现元素确实出现了但表格里的数据还是空的因为后端接口还没返回数据。用presence_of_element_located等待的只是 DOM 节点存在不等同于数据填充完成。这有点像 C 里new返回了非空指针但对象还没完成构造。我推荐的做法是等待业务数据特征出现比如等待表格某一行文本更新为预期值WebDriverWait(driver, 15).until( EC.text_to_be_present_in_element((By.CSS_SELECTOR, table#result tbody), 订单编号))或者干脆显式等待一个加载完成提示消失WebDriverWait(driver, 15).until( EC.invisibility_of_element_located((By.CSS_SELECTOR, div.loading)))不要小看这几个等待条件的区别用对了测试从随机挂变成稳定过。6.3 浏览器状态残留一个 session 用完一定要销毁最后提醒一个所有脚本工程师都会忽略的问题浏览器缓存和登录状态残留。如果你用同一个浏览器配置跑多次测试第二次运行时可能直接跳过了登录页因为 cookie 还在。这会导致你的登录测试用例时好时坏。解决办法是每次测试启动时使用独立的用户数据目录或者直接使用无痕模式启动 Chromefrom selenium import webdriver from selenium.webdriver.chrome.options import Options options Options() options.add_argument(--incognito) driver webdriver.Chrome(optionsoptions)如果你需要登录状态的复用就手动保存 cookie 并在下次启动时加载不要让浏览器自动记住乱套。结合 ChromeDriver 版本匹配方案、等待条件、元素定位这套东西捋顺了你在团队里就是那个自动化测试比较稳的人。我个人在实际项目中的体会是C 背景做 Web 自动化测试最大的优势不是代码写得有多花哨而是你天然带着资源管理生命周期异常安全这些严谨的思维习惯。当脚本工程师还在不断给测试用例加time.sleep(10)碰运气时你会本能地构建一套稳定、可维护、可诊断的测试体系。这个过程不是重新学一门语言而是把你已有的 C 工程素养迁移到一个新领域而且这条路的回报率相当可观。
返回列表