
“实验五 自动化测试”——这七个字摆在实验指导书上比前四个实验加起来都唬人。前四个实验无非是等价类划分、边界值、判定表、白盒覆盖纸笔加一份 Word 就能交差到了实验五突然要求你交出“能跑起来的东西”于是很多人打开编辑器就卡在第一条 import 上。我带过几届学生的软件测试课程实验也在实际项目里写过上万行自动化用例最大的感受是这个实验真正考的从来不是你会不会敲 Selenium 的那几个 API而是你能不能把一条手工测试用例拆解成机器可执行、失败可定位、换个人接手也能看懂的东西。说白了脚本只是外壳用例设计的思路和调试的耐心才是内核。下面我把这个实验从头到尾捋一遍先讲清楚它到底要你证明什么再讲选型和环境里那些没人明说但一定会绊你一脚的细节然后是“手工用例→自动化脚本”的完整翻译过程、断言与参数化的组织方式、我实打实踩过的坑和排查链路最后说说实验报告怎么写才不像流水账以及从这个实验往外延伸能走多远。不管你是第一次接触自动化测试的学生还是工作几年想补一补基本功的同行这篇都能当个参考。1. 先把题目拆开看实验五要你交出的其实是一条证据链1.1 自动化测试在课程体系里的位置很多人把自动化测试理解成“用手写的方式跑一遍手工测试”这个理解不算错但太浅了。手工测试的核心产出是“我点了、我看了、我判断了”判断过程在人的脑子里自动化测试的核心产出是“机器点了、机器比了、机器给出了明确结论”判断过程被固化成了断言。这两者最大的差别不是速度而是可重复性和可追溯性。举个最朴素的例子一个登录功能手工测试员今天测是“输入正确账号密码能进首页”明天换个测试员测可能就变成“能进后台首页且用户名显示正确”。判断标准悄悄变了但没人发现。写成自动化用例之后判断标准被锁死在代码里谁改了断言Git 记录一目了然。所以实验五安排在这个位置意图很明显前面几个实验训练你“怎么设计用例”实验五训练你“怎么让用例自己跑起来”。它是从“测试思维”走向“测试工程”的那道门槛。1.2 一份能拿分的实验成果应该包含什么我见过太多人把实验五做成了“代码提交作业”一个 .py 文件跑通了截图交上去。这样拿个及格分没问题但拿不到高分因为你没有交出完整的证据链。一个合格的自动化测试实验应该包含这几样东西交付物作用常见缺失点自动化用例清单说明测了什么只列脚本名不写覆盖的功能点可执行脚本核心产物硬编码、无注释、跑一次就废执行日志证明真的跑过只截图最后一行绿字测试报告结构化结果不知道用 pytest-html 之类的工具失败分析体现调试能力跑失败就删掉那条用例手工与自动对比体现思考深度完全没有这一节最后一行那栏是拉开分差的关键。指导书里通常不会明说要写对比但你只要补上一段“同样的 8 条用例手工执行约 25 分钟自动化首次执行 3 分 40 秒二次执行 40 秒”老师一眼就能看出你理解了自动化的价值在哪里——价值在重复执行不在第一次。1.3 什么该自动化什么别碰这是实验里最容易被忽略、但在工作中最值钱的一个判断。不是所有用例都值得写成脚本判断标准大致是三条执行频率高不高、步骤是否稳定、结果是否可客观判定。适合登录、注册、查询列表、下单主流程、接口返回字段校验、金额计算。不太适合纯视觉判断这个按钮颜色对不对、探索性测试、需求还在天天变的新功能、只跑一次的一次性验证。绝对别碰验证码识别除非用测试环境万能码、依赖第三方短信/支付回调的真实链路。提示实验环境里如果碰到验证码正解是找开发同学在测试环境加个万能验证码或者开关而不是去研究图像识别。这个思路在工作中同样成立。2. 选型这一步别乱Web、接口、移动端三条路线的取舍2.1 三条主流路线的适用边界自动化测试这个词太大落到具体实验上通常只会让你选一条路走。我按“上手成本”和“实验常见度”排一下路线典型技术栈上手难度实验出现频率Web UI 自动化Python Selenium / Playwright中最高接口自动化Python requests pytest低高移动端自动化Python/Java Appium高较低如果你的实验指导书没有硬性指定我建议的优先级是接口 Web UI 移动端。原因很实在——接口层没有渲染等待、没有元素定位、没有浏览器版本兼容能让你把精力集中在“用例组织”和“断言设计”上这恰恰是实验真正想训练你的能力。而 UI 自动化 70% 的调试时间都花在“元素找不到”上对新手极不友好。但如果你是移动端方向或者实验室设备齐全Appium 也不是不能做只是要提前把 Android SDK、环境变量、模拟器这几样东西配好光是环境就能吃掉你半天。2.2 环境搭建里最容易忽略的四个细节环境这关我总结下来有四个坑年年有人踩第一个是虚拟环境。直接 pip install 到全局 Python 里后面项目一多就互相打架。养成习惯python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate pip install selenium pytest pytest-html第二个是浏览器驱动版本。老教程会教你手动下载 chromedriver 然后放到 PATH 里现在完全不需要了——Selenium 4.6 之后内置了 Selenium Manager会自动匹配驱动。你要是还在手动下载很可能版本对不上报一个SessionNotCreatedException。第三个是浏览器自动更新。这是最阴的一个坑你今天跑得好好的明天 Chrome 悄悄升了个版本驱动不匹配了。实验周期一般一两周正好撞上。规避办法有两个用 Selenium Manager 自动匹配或者干脆固定浏览器版本、关掉自动更新。第四个是编码。Windows 下默认 GBK读中文测试数据文件时容易乱码脚本开头统一加一行# -*- coding: utf-8 -*-并在读文件时显式指定encodingutf-8能省掉一堆莫名其妙的报错。2.3 一个 30 秒能验证环境是否可用的最小脚本别急着写完整用例先跑通这个最小闭环确认浏览器能起来、页面能打开、能拿到标题from selenium import webdriver from selenium.webdriver.common.by import By driver webdriver.Chrome() try: driver.get(https://www.saucedemo.com/) assert Swag Labs in driver.title print(环境 OK标题为, driver.title) finally: driver.quit()这段代码的重点在finally里的quit()。新手最常见的翻车现场就是脚本报错后浏览器窗口堆了一屏手动关半天。养成把quit()放在finally里的习惯受益终身。3. 把一条手工用例翻译成自动化脚本的完整过程3.1 先拆用例再写代码这是我最想强调的一点打开编辑器之前先把用例写成文字。一条标准的手工用例长这样用例编号LOGIN-002 用例标题使用正确账号密码登录 前置条件浏览器已打开登录页账号 testuser 已注册 操作步骤 1. 在用户名输入框输入 testuser 2. 在密码输入框输入正确密码 3. 点击登录按钮 预期结果 1. 页面跳转到商品列表页 2. 右上角显示用户名 testuser把它“翻译”成脚本的过程其实是把每一句话对应成一个动作或一个断言用例语句脚本映射类型在用户名框输入find_element(...).send_keys()操作点击登录find_element(...).click()操作跳转到商品列表页assert /inventory in driver.current_url断言显示用户名assert username testuser断言很多人写脚本卡壳不是因为不会写 API而是因为用例本身写得含糊——“正常登录”“页面正常显示”这种描述机器没法执行。用例写得越具体脚本越好写这个因果关系要反过来想。3.2 元素定位的优先级别一上来就 XPath定位方式有好几种按我的实测优先级排id唯一、最快、最稳有就用。name />driver.find_element(By.XPATH, //input[data-testusername])我个人的建议是跟开发约定加上稳定的 test 属性这比任何定位技巧都管用。实验里做不到的话至少把定位器抽成变量放在文件头部页面变了只改一处。3.3 显式等待比 sleep 靠谱不是一点点新手最爱写time.sleep(3)理由是“等页面加载完”。问题是快的机器上浪费 3 秒慢的机器上照样失败。更糟的是一个用例里塞五六个 sleep十个用例跑下来就是十几分钟自动化的速度优势全没了。正确的做法是显式等待from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC wait WebDriverWait(driver, 10) login_btn wait.until( EC.element_to_be_clickable((By.ID, login-button)) ) login_btn.click()它的逻辑是“最多等 10 秒元素一出现立刻往下走”默认每 0.5 秒轮询一次。速度快、稳定性高这才是应该写进实验报告的东西。顺便说下隐式等待driver.implicitly_wait(10)它只管“元素存在于 DOM”不管“元素可见”“元素可点击”。所以经常出现“元素找到了但点不了”的情况。我的用法是全局设一个 3 到 5 秒的隐式等待兜底关键操作再叠加显式等待。4. 从“能跑”到“能维护”断言、参数化和测试组织4.1 断言写得好不好决定用例的含金量初学者最容易犯的错是用print代替断言# 反面教材 print(driver.find_element(By.CLASS_NAME, title).text)这行代码跑了之后你得用眼睛看输出、用脑子判断对错。这跟手工测试的区别在哪机器只帮你点了鼠标判断还是人在做。正确写法是让断言来判title driver.find_element(By.CLASS_NAME, title).text assert title Products, f期望标题为 Products实际为 {title}断言里的提示信息一定要带上“期望值”和“实际值”。失败的时候这行信息就是你的第一手线索比重新跑一遍调试快得多。还有个经验一条用例里的断言不要太多。我见过有人一个脚本里塞二十个断言跑到第十五条挂了前十四条的状态完全不知道。合理的粒度是 2 到 4 个关键断言覆盖“操作是否成功”和“结果是否正确”就够了。4.2 参数化把用例表变成数据而不是复制粘贴假设你要测登录的 5 种情况正确账号密码、密码错误、用户名为空、密码为空、账号被锁定。新手会写五个函数代码重复 90%。参数化之后是这样import pytest pytest.mark.parametrize(username, password, expected, [ (standard_user, secret_sauce, success), (standard_user, wrong_pwd, error), (, secret_sauce, error), (standard_user, , error), (locked_out_user, secret_sauce, error), ]) def test_login(driver, username, password, expected): driver.get(https://www.saucedemo.com/) driver.find_element(By.ID, user-name).send_keys(username) driver.find_element(By.ID, password).send_keys(password) driver.find_element(By.ID, login-button).click() if expected success: assert /inventory in driver.current_url else: err driver.find_element(By.CSS_SELECTOR, [data-testerror]) assert err.is_displayed()这么写有三个直接好处用例条数一目了然、加一条用例只需要加一行数据、执行报告里每条数据单独统计结果。实验报告里贴上这段代码比贴十个重复函数体面得多。数据量大的时候把数据外置到 CSV 或 YAMLimport csv, pytest def load_cases(path): with open(path, encodingutf-8) as f: return [(r[username], r[password], r[expected]) for r in csv.DictReader(f)] pytest.mark.parametrize(username,password,expected, load_cases(data/login_cases.csv)) def test_login(driver, username, password, expected): ...这就是“数据驱动”的雏形。好处是测试人员改用例不用碰代码这在真实项目里是刚需。4.3 报告和失败截图一个都别省实验里跑完脚本如果只有控制台输出说服力是很弱的。两个工具能显著提升交付质量pytest-html一行命令出报告pytest test_login.py --htmlreport.html --self-contained-html生成的 HTML 里有每条用例的通过状态、耗时、失败堆栈直接当实验报告的附件。失败自动截图靠 fixture 实现import pytest pytest.fixture def driver(): d webdriver.Chrome() yield d d.quit() pytest.hookimpl(hookwrapperTrue) def pytest_runtest_makereport(item, call): outcome yield report outcome.get_result() if report.when call and report.failed: d item.funcargs.get(driver) if d: d.save_screenshot(ffail_{item.name}.png)失败截图是排查 UI 问题的第一手材料。元素找不到的时候一张截图能告诉你页面是不是停在登录页、是不是弹了个公告框、是不是加载出了 404。5. 我踩过的坑以及完整的排查链路5.1 元素定位不到的六种真实原因“NoSuchElementException”这个报错我至少遇到过几十次真正的原因无非这六种按出现频率排排序原因判断方法解决方式1元素还没加载出来加 2 秒 sleep 后能跑通改成显式等待2定位器写错了用浏览器控制台$$(选择器)验证修正选择器3元素在 iframe 里爬到页面顶部看结构无该元素switch_to.frame()4元素在新标签页driver.window_handles数量 1切换窗口句柄5元素被弹窗遮挡截图能看到浮层先关弹窗6页面是懒加载元素在下方未滚动scrollIntoView()排查顺序就按这个表从上往下走别跳。我见过有同学一开始就去研究 iframe折腾一小时最后发现是没加等待。5.2 偶发失败flaky怎么定位比“一直失败”更让人崩溃的是“十次跑八次成功”。这类问题通常有三个来源等待不充分把关键的显式等待时间从 5 秒提到 15 秒看还复现不。数据污染上一条用例留下的数据影响了这一条比如注册用例重复执行导致“用户名已存在”。解决思路是用时间戳生成唯一数据。执行顺序依赖用例 A 依赖用例 B 先执行。这是设计缺陷每条用例都应该能独立跑。定位 flaky 有个笨办法但极其有效把这条用例单独循环跑 20 遍。如果 20 遍都过大概率是数据或顺序问题如果偶发失败那基本可以锁定在等待或网络。pytest test_login.py::test_login -x --count205.3 iframe、多窗口、弹窗三件套Web 自动化到中后期一定会撞上这三样。处理逻辑其实很像都是“先切进去操作完再切出来”。iframe 的处理wait.until(EC.frame_to_be_available_and_switch_to_it((By.ID, pay-frame))) # 在 iframe 内操作 driver.find_element(By.ID, confirm).click() driver.switch_to.default_content() # 切回主文档注意 iframe 必须用等待方式切直接switch_to.frame()在 iframe 还没加载完时会抛异常。多窗口的处理original driver.current_window_handle driver.find_element(By.LINK_TEXT, 帮助中心).click() wait.until(EC.number_of_windows_to_be(2)) new_win [h for h in driver.window_handles if h ! original][0] driver.switch_to.window(new_win) # 操作新窗口 driver.close() driver.switch_to.window(original)这里最容易被忽略的是driver.close()和driver.quit()的区别close()只关当前标签页quit()关掉整个浏览器。多窗口场景里前者才够用。6. 实验报告怎么写才不像流水账6.1 结构建议对齐评分点实验报告最忌讳两种写法一种是通篇代码截图加一句“运行成功”另一种是把指导书抄一遍。老师想看的其实是你的思考过程我建议按这个结构来实验目的与理解用两三句话说明自动化测试解决什么问题别抄书。测试对象与技术选型为什么选 Web 不选接口为什么选 Python 不选 Java给出理由。自动化用例设计直接贴你前面那张“用例语句→脚本映射”的表格非常有说服力。核心技术实现元素定位策略、等待机制、断言设计、参数化组织每块配一段解释。执行结果报告截图 通过率 耗时统计。问题与解决这一节是分水岭把踩坑过程写出来展示你的排查思路。效率对比与不足手工 vs 自动的耗时对比以及本次脚本还有哪些覆盖不到的地方。第七节里的“不足”千万别省。主动说出“本脚本未覆盖并发登录场景”“依赖固定测试数据、未做数据清理”说明你真的动脑子了比硬夸自己覆盖全面得分高。6.2 证据留存的小技巧截图带时间文件命名用日期_用例编号_步骤比如0512_LOGIN002_失败_元素未找到.png。保留失败案例一条用例失败然后被你修好这个“失败→修复”的过程是很好的素材别删。日志分级关键步骤打 INFO定位信息打 DEBUG别一股脑全打 print。代码加注释每个函数头部写清楚这个函数做什么、依赖什么前置条件。老师扫一眼就能看懂这本身就是加分项。7. 从这个实验往外走路比想象中长7.1 Page Object 模式到底解决什么问题实验规模的脚本十几个元素定位写在函数里没问题。但真实项目动辄几十个页面、几百条用例同一个登录按钮的位置可能出现在三十个文件里。前端一改你得改三十处。Page Object 的思路很朴素把页面封装成类元素定位和操作都放在类里。class LoginPage: URL https://www.saucedemo.com/ def __init__(self, driver): self.driver driver self.username (By.ID, user-name) self.password (By.ID, password) self.submit (By.ID, login-button) def open(self): self.driver.get(self.URL) return self def login(self, user, pwd): self.driver.find_element(*self.username).send_keys(user) self.driver.find_element(*self.password).send_keys(pwd) self.driver.find_element(*self.submit).click() return InventoryPage(self.driver)页面结构变了只改LoginPage一处用例里写的是login_page.login(standard_user, secret_sauce)读起来就是业务语言。这就是为什么面试里问自动化Page Object 几乎必考——它考的是你有没有工程化思维而不是 API 熟练度。7.2 把脚本挂上流水线的最小可行方案实验做完脚本躺在本地是很可惜的。最小可行的做法是写一个批处理或 shell 脚本配合定时任务每天跑一次#!/bin/bash cd /path/to/project source venv/bin/activate pytest tests/ --htmlreports/report_$(date %Y%m%d).html --self-contained-html再往上一步就是接到持续集成工具里每次代码提交自动触发。这一步在实验里通常不要求但如果你能写进报告的“后续优化”一节说明你已经看到了自动化测试真正的价值点它必须被频繁执行才有意义一周跑一次还不如不做。7.3 面试里被问到自动化怎么答才落地这个实验的内容和工作里的自动化面试题重合度很高。几个高频问题我顺手答一下“你们自动化覆盖率多少”——别编数字说清楚覆盖的是哪部分比如“核心主流程全覆盖回归用例覆盖约 60%异常分支以手工为主”。“脚本不稳定怎么处理”——从等待机制、数据隔离、用例独立性三个角度答再补一句“我们会统计近两周的 flaky 率超过 5% 的用例必须重构”。“UI 自动化和接口自动化怎么选”——接口优先UI 只保留最核心的主流程。理由是 UI 维护成本高、执行慢、稳定性差。这三个问题的答案其实都能从你做完实验五之后的真实体会里提炼出来。实验本身不会直接给你 offer但它给了你一套可复用的方法论把需求拆成用例把用例拆成动作和断言把动作和断言组织成可维护的代码再让代码自动、重复、稳定地跑起来。这套东西换到任何测试岗位上都是通用的。最后分享一个我一直在用的小习惯每次写完一组脚本先故意制造一次失败——把断言里的期望值改错跑一遍看报错信息够不够清晰看截图有没有正常生成。如果报错信息让你一眼能定位到问题说明这组脚本写合格了如果看半天不知道哪里错了那它在别人手里只会变成负担。这个“负向验证”的习惯比多写十条用例更能提升脚本质量。