
不需要从“安装什么软件”这种老生常谈开始因为我觉得真正卡住大多数新人的往往不是安装而是“装上之后不知道下一步干嘛”。这篇文章我就从一个更务实的角度出发结合我这些年先后做过Web端UI自动化、App端自动化以及维护过几套大型自动化测试框架的经验讲讲怎么真正开始用selenium做自动化测试以及从“能跑脚本”到“有一套能用的测试框架”中间到底要跨过哪些坑。1. 先想清楚你真正需要的是哪种自动化1.1 自动化测试解决的核心问题很多人一说自动化测试第一反应就是Selenium。没错Selenium确实是Web UI自动化测试领域最经典、最流行的工具它通过驱动浏览器模拟真实用户逐一点击、输入、跳转、断言这些操作帮你把“手工点页面”这件事变成“机器跑脚本”。但开始动手之前我建议你先想清楚一个问题你到底是来学Selenium这个工具还是来解决“回归测试效率太低”这个实际问题我见过不少新人埋头学了一个月Selenium把各种API背得滚瓜烂熟结果到了项目里发现根本做不下去。为什么因为自动化测试的本质是可重复、可验证、可维护而不是“我会用某个工具”。拿我自己的经历举例。我刚入行那年接手的项目是一个后台管理系统功能多到离谱光是菜单就有几十个每次发版本之前测试组要花整整两天做全量回归。领导拍板说“上自动化”当时大家都很兴奋觉得终于可以解脱了。结果呢第一个月用例写了一堆但每次跑完看一眼报表就完事没人愿意去维护。第二个月页面一改版脚本挂掉一片修复的速度永远赶不上开发改页面的速度。到第三个月这套自动化已经成了摆设。真正让我想明白的是在一个很偶然的瞬间。那天我手动测试时重复点了20多次“新建订单”流程点得自己都快要吐了突然意识到手动测试虽然慢但它有一个自动化很难替代的优势——人会思考脚本不会。脚本只知道“按着固定路径走”一旦界面变了、数据变了、网络慢了它就死给你看。所以Selenium不是用来替代人的它是用来把那些“重复、稳定、可预期”的回归路径固化下来的。如果你测试的业务场景天天变、页面天天改、数据还不稳定那再强的工具也救不了你。1.2 什么时候不该上Selenium这个话题听起来有点劝退但我觉得它比“怎么用Selenium”更重要。我见过太多团队为了自动化而自动化最后得不偿失。先说结论**如果你只是临时验证一两个页面、测试环境三天两头重置、业务逻辑频繁变化或者团队里只有你一个人懂代码那我不建议急着上Selenium。**这话说出来可能会让很多新人失望但确实是我的真实感受。自动化的收益曲线不是线性的它有一个很长的“投入期”前期写脚本、调试、封装、维护的成本比手动测试高多了。它的收益要在用例数量够多、回归频率够高的前提下才会慢慢显现。那什么时候该上呢我的判断标准很简单三条有稳定的测试环境测试数据可控可重建有相对固定的核心回归路径比如登录、下单、支付这种高频场景团队愿意投入时间维护脚本而不是指望“写完一劳永逸”这三条缺一条我建议你先别动手回去把基础工作补上否则你后面会做得非常痛苦。我做过一个项目测试环境是大家共用的开发在里面随便改数据动不动就清库我的自动化脚本每次跑失败查半天发现是环境数据被改掉了。后来我花了两周时间推动测试环境独立化、测试数据脚本化从那以后自动化才真正跑出价值来。1.3 选Python还是Java其实没你想的那么重要很多新人纠结第一门语言选什么。Selenium官方支持Python、Java、C#、JavaScript等等说实话选哪个都不影响你“入门Selenium”真正影响你的是“你所在团队的技术栈”和“你以后打算做多深”。我的建议如果你的团队技术栈是Java为主那你就用Java这样后续跟开发协作、走持续集成CI流程会更顺畅。如果团队没限制你个人也是零基础那我更推荐Python。原因很简单Python上手快写起来代码量少调试也直观。你可以把更多精力放在理解自动化测试的思路本身而不是跟编程语言较劲。不过要提醒一句Selenium只是自动化测试体系里的一块拼图后面你大概率还会接触pytest、allure、selenium-grid这类生态工具。Python在这方面的生态非常成熟尤其pytest配合Selenium几乎是国内测试圈的标配组合。后面我也会重点讲这套组合怎么落地。2. 环境搭建从零到能跑通第一个脚本2.1 搭建一个干净且可靠的Python环境如果你决定用Python第一步不是急着pip install selenium而是先把Python本身的环境理清楚。我强烈建议你装Python 3.8以上的版本然后用虚拟环境venv或virtualenv来隔离项目依赖。为什么非要搞虚拟环境因为你的电脑上可能同时有好几个Python项目它们的依赖版本可能互相冲突今天装这个把那个搞坏了你查半天都不知道怎么回事。每个项目一个独立的环境互不干扰干净省心。具体操作很简单在项目目录里执行python -m venv selftest_envWindows下激活虚拟环境selftest_env\Scripts\activateMac/Linux下激活虚拟环境source selftest_env/bin/activate激活之后你就发现命令行前面多了一个(selftest_env)前缀这说明你已经进入了一个独立的Python世界。接下来装Selenium就非常简单了pip install selenium这里顺便多说一句现在Selenium已经到了4.x版本API比老版本更简洁而且内建了相对稳健的元素等待支持后面讲等待机制的时候我会详细说。如果你参考老教程时发现代码报错看看是不是版本差异导致的API调用变了。2.2 浏览器驱动整个环境里最大的坑安装好Selenium库只是第一步真正让Selenium能够控制浏览器的是浏览器驱动。以Chrome为例你需要下载对应版本的chromedriver而且要跟你的Chrome浏览器版本严格匹配。这个匹配关系我一开始也吃过不少亏——版本不匹配的时候浏览器能打开但瞬间就闪退了报错信息还特别不直观。现在比较推荐的方案是使用webdriver-manager这个库它会自动检测你本地的浏览器版本然后下载对应的驱动省去了手工寻找版本的痛苦pip install webdriver-manager使用的时候也很简单from selenium import webdriver from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.chrome.service import Service service Service(ChromeDriverManager().install()) driver webdriver.Chrome(serviceservice)这样你就不用关心chromedriver放在哪里、版本对不对全自动搞定。如果你用的是Edge或Firefox机制也类似分别对应EdgeChromiumDriverManager和GeckoDriverManager。2.3 第一个能跑通的Selenium脚本环境装好之后我建议你先别急着写复杂的测试逻辑先跑通下面这个最简单的脚本确认Selenium能成功打开浏览器、加载页面、关闭浏览器这一步顺畅了后面才有意义from selenium import webdriver from selenium.webdriver.common.by import By from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.chrome.service import Service service Service(ChromeDriverManager().install()) driver webdriver.Chrome(serviceservice) try: driver.get(https://www.baidu.com) print(页面标题是, driver.title) assert 百度 in driver.title finally: driver.quit()这个脚本虽然简单但里面的try...finally结构非常关键。driver.quit()一定要放在finally里否则一旦断言失败或者脚本中途报错浏览器窗口就会一直留在那里不关闭。时间长了你的电脑上几十个僵尸浏览器窗口既占内存又影响后续测试全靠这个习惯来避免。跑通这个脚本之后你就算正式迈进Selenium的大门了。不过别急着高兴这才是开始。接下来要面对的是整个自动化测试里最核心、也最让新人头疼的部分元素定位。3. 元素定位自动化测试的命根子3.1 八种定位方式怎么选Selenium的核心操作思路很简单找到页面上的元素然后对它执行操作。怎么找到它官方提供了八种方式id、name、class name、tag name、link text、partial link text、xpath、css selector。很多初学者到这里就懵了这么多方式到底用哪个我的经验是优先用id没有id就写css selector实在不行才上xpath别一上来就抄xpath。为什么因为id在正常情况下是唯一的代码最简洁、定位最稳定。但很多现代前端框架尤其Vue、React开发的页面越来越不爱写id反而大量使用动态的class这时候你就得靠css selector来精准定位。举个例子假设你要定位一个登录按钮HTML长这样button idloginBtn classbtn btn-primary login-button登录/button用id定位就是一行代码driver.find_element(By.ID, loginBtn).click()如果没有id用css selector可以这样写driver.find_element(By.CSS_SELECTOR, button.login-button).click()至于xpath它的优势是功能强大、语法灵活尤其适合元素嵌套很深或者你只想根据文本内容找元素的情况。比如driver.find_element(By.XPATH, //button[contains(text(),登录)]).click()这种写法就比较“赖皮”了——我不关心你在页面哪个位置只要按钮文字是“登录”我就点你。对于文本经常变化的页面可以用contains(text(),...)这种模糊匹配方式但注意不要用得太随意否则很容易误点多匹配到的元素。3.2 等待机制为什么显式等待才是首选这一节我多说两句因为等待机制是新手写的脚本和资深工程师写的脚本最大的分水岭。我见过太多新人写的脚本driver.find_element(By.ID, loginBtn).click() driver.find_element(By.ID, username).send_keys(admin)看起来没错吧但一跑起来就时不时报错NoSuchElementException。原因很简单页面加载是需要时间的尤其是现在的前端页面大量使用Ajax异步加载你点击登录按钮之后用户名输入框可能过了一两秒才渲染出来。脚本执行速度远比你手动操作快得多它不会等页面上正在加载的元素。有些新人会想到用time.sleep(3)来硬等我劝你趁早放弃这个习惯。sleep是一个定时炸弹网络快的时候浪费三秒网络慢的时候三秒根本不够脚本要么慢死要么照样报错。正确做法是用显式等待也就是让脚本不断轮询直到某个条件成立或者超过最大等待时间from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC wait WebDriverWait(driver, 10) login_input wait.until(EC.presence_of_element_located((By.ID, username))) login_input.send_keys(admin)WebDriverWait负责每隔一小段时间默认0.5秒去检查一次页面只要你指定的元素一出现立刻往下走最多等10秒。这样既不会浪费时间也不会因为网络波动而误报。你可以把wait对象封装成一个公共方法或者放在fixture里整个测试类共用。我再补充一下presence_of_element_located和visibility_of_element_located的区别新人经常搞混。前者只检查元素存不存在于DOM树里哪怕它在页面中是隐藏的也能通过后者还要求元素可见且不是disabled状态。大多数情况下你要跟用户一样操作一个元素用的是后者比如点击一个按钮、往输入框里打字元素必须真的可见才行。3.3 常见定位失败的排查思路定位失败是自动化测试里出现频率最高的问题我把最常见的几种情况和排查思路整理一下现象可能原因排查建议报NoSuchElementException元素还没加载出来换成显式等待别急着定位定位到一个但操作报错iframe中嵌套了目标元素先切换到iframe再用原始定位方式元素明明存在但点不动元素被遮罩层遮挡或属性是disabled用js绕过遮挡层或检查元素状态属性用xpath复制出来的定位失败前端框架动态重写了DOM改用id、css属性等更稳定的定位方式多个元素重复匹配一个页面上有多个相同class用CSS选择器配合层级关系或通过索引选取排查的时候多用浏览器开发者工具F12打开先在Console里验证你的选择器能不能选到对应的元素。我自己的习惯是先让driver在报错前截图保留下现场再结合HTML里的实际结构反推问题出在哪。窗口里看不太清的时候就把页面的page_source存成HTML文件用浏览器打开慢慢看。4. 把脚本升级成一套可维护的测试框架4.1 用Pytest接管测试用例的执行跑通脚本不难难的是把脚本变成一个“框架”。什么叫框架简单说就是有组织、有规范、可复用、可报告。如果你还是把测试逻辑全写在一个def main()里从头到尾顺序执行那说实话等于没入门。我建议你从第一天起就引入pytest。它是目前Python生态中最主流的测试框架功能强大插件丰富配合Selenium简直完美。安装pip install pytest pytest-html用pytest组织用例很简单。比如你写了一个文件test_login.py里面放几个测试函数def test_login_success(): # 测试登录成功场景 pass def test_login_failed_wrong_password(): # 测试密码错误场景 pass然后在项目根目录执行pytest -vpytest就会自动发现所有test_开头的函数或Test开头的类逐个执行并输出报告非常方便。它还天然支持断言就是Python自带的assert不需要像Java里的TestNG那样写一堆额外断言库。4.2 fixture管理浏览器生命周期pytest里面对Selenium最有用的是fixture它是用来管理测试前置和后置逻辑的。最典型的应用场景就是浏览器对象的创建与销毁。你肯定不希望每个测试用例都写一遍“启动浏览器、访问网址、关闭浏览器”所以用一个fixture把它们统一管理起来import pytest from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager pytest.fixture(scopefunction) def driver(): service Service(ChromeDriverManager().install()) driver webdriver.Chrome(serviceservice) driver.implicitly_wait(5) yield driver driver.quit()这里yield之前的代码是前置逻辑启动浏览器yield driver把浏览器对象传给测试函数yield之后是后置逻辑关闭浏览器。每个测试函数只要在参数里声明driver就能自动拿到一个全新的浏览器实例用完自动关闭非常优雅。如果你有多个测试模块可以把fixture放到conftest.py文件里这样模块之间可以共享不用重复定义。关于scope参数我简单说明一下function表示每个测试函数独立一个浏览器隔离性好但慢session表示整个测试会话共用一个浏览器快但用例之间会残留登录态。这个取舍看你测试的具体需求我一般默认用function安全第一。4.3 参数化用一份数据跑多组用例Selenium自动化测试最典型的应用场景就是用同一套测试步骤验证多组输入数据。pytest里这个功能叫parametrize。举个例子你要测登录功能的多个用户场景import pytest from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC pytest.mark.parametrize(username,password,expected, [ (admin, 123456, 登录成功), (user1, wrong_pwd, 用户名或密码错误), (, 123456, 用户名不能为空), ]) def test_login_scenarios(driver, username, password, expected): driver.get(http://your-test-site.com/login) WebDriverWait(driver, 10).until( EC.visibility_of_element_located((By.ID, username)) ).send_keys(username) driver.find_element(By.ID, password).send_keys(password) driver.find_element(By.ID, loginBtn).click() result WebDriverWait(driver, 10).until( EC.visibility_of_element_located((By.ID, resultMsg)) ).text assert expected in result这样一来三组测试数据就被共享到同一个测试函数里逻辑只写一遍。后面如果你的测试数据越来越多还可以把数据存到JSON文件、Excel表格里配合pytest的fixture做数据驱动测试体量扩大后这套模式的优势会越来越明显。4.4 失败了不能白失败截图与报告测试跑不过不可怕可怕的是跑完了你都不知道为什么没跑过。这是很多团队自动化测试最后沦为摆设的第二个原因——报告还停留在“红绿灯”水平绿灯亮了没人关心红灯亮了也没人想去修。我的建议是至少在两种情况下保留证据一是断言失败时二是测试报错时。pytest里最简单的方式是配合pytest-html插件但截图这件事需要自己处理。我习惯写一个全局的异常钩子在任何用例失败时自动截图pytest.hookimpl(hookwrapperTrue) def pytest_runtest_makereport(item, call): outcome yield report outcome.get_result() if report.when call and report.failed: driver item.funcargs[driver] timestamp time.strftime(%Y%m%d_%H%M%S) driver.save_screenshot(freports/{item.name}_{timestamp}.png)这段代码用到了pytest的hook机制通俗理解就是每个用例执行完我检查一下结果如果这一步执行阶段是call即真正执行阶段而且状态是failed就自动把当前浏览器画面存成一张图片放到reports目录里。配合pytest-html生成的HTML测试报告你能看到每个用例的状态也能直接附上失败截图路径排查问题效率直接翻倍。更专业一点的团队会引入Allure报告它的展示效果确实更好树形结构、历史趋势、失败分类都很完善但配置成本也高一些。我的建议是先把pytest-html用顺手跑过几个真实项目历练一下再考虑上Allure不迟。5. 常见问题与排查技巧实录5.1 iframe、弹窗、新窗口三大拦路虎我把Selenium实战里最让人头疼的三类问题拎出来单独讲因为它们真的能让老手都卡上半天。第一类是iframe。现在很多系统用iframe做嵌套页面比如在线编辑器、第三方支付组件。你以为定位到的元素在正常页面上其实它在iframe内部这时你直接find_element是找不到的。必须先用driver.switch_to.frame()切进去driver.switch_to.frame(iframe_id_or_name) # 找到iframe内部的元素并操作 driver.find_element(By.ID, content).send_keys(hello) # 操作完了记得切回主文档 driver.switch_to.default_content()第二类是弹窗。Alert弹窗是浏览器原生的Selenium操作方式是driver.switch_to.alert如果弹窗是网页自定义的div模拟弹窗那就本质上还是一个普通元素用正常定位方式处理即可。很多人混淆了这两类弹窗想着用alert去关一个页面自己渲染的模态框怎么试都不对。第三类是新窗口。点击一个链接打开新Tab这时候Selenium仍然停留在旧窗口你要切换过去original_window driver.current_window_handle driver.switch_to.new_window(tab) # 操作完新窗口之后 driver.switch_to.window(original_window)如果是点击某个元素之后自动打开新窗口可以先等一等窗口数变化from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC WebDriverWait(driver, 10).until(EC.number_of_windows_to_be(2)) new_window [w for w in driver.window_handles if w ! original_window][0] driver.switch_to.window(new_window)这地方很多老手都容易翻车因为新版Selenium窗口操作API有调整老代码可能失效。如果你用的Selenium4.xdriver.switch_to.window()之前还是先查一下当前可用API。5.2 测试不稳定随机失败怎么查我见过最让人崩溃的不是测试全挂而是同一个用例这次跑通过下次跑挂掉再下次又通过。这种随机性强的失败最浪费调试时间。结合我的经验80%的“随机失败”其实都可以归结到几个根因上页面加载时机不确定没有用显式等待或者在等待条件里用错了判断方式。测试数据互相污染。比如前一个用例创建的订单影响了后续用例的数据统计。浏览器窗口焦点问题。Headless模式无头浏览器下某些元素被认为不可点击。执行环境性能波动。CI机器性能差导致显式等待的10秒都不够用。排查随机失败我的建议是先把失败现场保留好。上文提过截图非常关键。另外你还可以在重试时把页面HTML打下这样不用反复盲猜。一个常用的技巧是给关键操作的等待时间加日志比如打印“开始等待登录按钮当前页面标题是xxx”这样一遍遍跑下去就能感受到页面状态的差异。如果确实是环境波动造成的偶发失败还可以考虑引入pytest重试机制比如pytest-rerunfailures插件pip install pytest-rerunfailures执行用例的时候加上参数--reruns 2意思是一个用例失败后再让它最多重跑2次只有连续失败才算真失败。这个功能能帮你过滤掉很多环境原因的偶发问题让真正需要关注的稳定失败浮出水面。5.3 执行速度与稳定性如何平衡随着用例数量增加你会遇到新的问题跑全量用例要半小时甚至更久跑太慢以至于没人愿意频繁执行。这时候有两个方向可以优化。第一个方向是并发执行。pytest可以配合pytest-xdist插件实现用例的并行执行pip install pytest-xdist pytest -n 4-n 4表示同时启动4个进程每个进程里跑不同的用例整体耗时能缩短到原来的四分之一左右。但要注意并发跑Selenium用例时多个浏览器同时操作同一个系统可能会触发服务端的数据冲突需要你自己确认一下系统允不允许这种并发操作。我之前就因为没做这个确认导致并发一上就大面积失败。第二个方向是合理取舍。不是所有用例都应该跑在UI层尤其是一些纯接口校验、数据计算逻辑放在接口自动化更合适。UI自动化只保留那些真正需要“端到端”验证的核心场景。很多人被“100%自动化”的口号忽悠非要把所有用例都迁到UI上去最后维护成本大得惊人。我做过的项目里UI自动化的用例数是不多的大概20到40条核心路径剩下的都压在接口层和单测层整体效率和稳定性反而更好。6. 写在最后先学着让脚本跑起来再谈框架6.1 我从新人到老手踩过的几个坑回想我自己从第一次接触Selenium到能带团队搭建完整的自动化体系中间踩过的坑其实比这本书上写的多得多挑几个有代表性的说说。刚学Selenium的时候我花了一整晚折腾chromedriver的版本问题后来才发现是Chrome自动更新了驱动没跟上。如果你也遇到“浏览器闪退”、“SessionNotCreatedException”这类报错先检查驱动版本。装个webdriver-manager能省掉90%的这类烦恼网上很多老教程里让你手动下载驱动的操作今天其实已经没必要了。后来我开始写真实项目用例时又踩了“测试用例互相影响”的坑。两个用例写了同一个账号登录第一个用例退出登录失败了第二个用例拿到的是一个残留的登录态结果居然通过了——这种“假通过”比“真失败”更可怕它会让你误以为系统没问题等到上线出了bug才追悔莫及。我现在做框架设计时一定会为每条UI用例准备独立的测试数据或者课上用例结束后强制清理状态。还有一个很多人忽略的地方浏览器窗口大小会影响元素可见性。同样的脚本在我自己电脑上1200宽的窗口跑得好好的到了CI服务器的默认窗口就报“元素不可点击”。后来我在fixture里统一设置了窗口尺寸driver.set_window_size(1920, 1080)这个问题就彻底消失了。你写用例的时候也要把这类“环境差异”考虑进去不然脚本换个机器就跑不起来了。6.2 后续可以怎么扩展如果你已经把Selenium的基本功练扎实了也有了稳定的pytest用例集你还可以往三个方向扩展。一是引入数据驱动。把测试数据从代码里剥离出来放到Excel或JSON里用例本身与数据解耦这样非技术同学也能维护测试数据。我见过不少团队最终都走到这一步因为纯代码维护用例门槛还是偏高。二是尝试Appium做移动端自动化。它的核心思路跟Selenium高度相似都是通过一个WebDriver协议去驱动移动设备。你如果Selenium底子好学Appium会非常快。这也是自动化测试职业路径上很自然的一个进阶方向。三是把自动化测试嵌进CI/CD流水线完成一次代码提交后自动跑回归测试并且把失败信息推送给相关责任人。我接触过的企业基本都会走到这一步这也是自动化价值最大化的最终形态。这些方向我都亲自实践过也都踩过不少坑以后有机会再分开写文章详细聊。最后说一句我自己的体会自动化测试的关键不是那些工具和API而是你有没有长期维护它的决心和习惯。工具更新可以学、脚本报错可以改但维护自动化体系的意识和问题定位的能力必须通过一次次真实项目的历练才能沉淀下来。如果你刚开始学别急着追求大而全的框架先让一个小脚本稳定跑通再一点点往上加东西这是我最推荐的路。