自动化测试进阶实战:显式等待、数据驱动与页面对象封装解析
2026/9/19 3:57:57 网站建设 项目流程

1. 项目概述与目标拆解

接到“3.1完成进阶13、14、15”这个任务时,我先愣了一下,因为这看起来就是一条非常简单的课程进度记录。但我把它当做一个“小项目”来对待,而不是随手划掉三道题。只有把任务拆开看透,才知道进阶13、14、15这三道题到底在考什么、怎么练才不白练、做完之后能沉淀下什么。

先交代一下背景:我所在的团队每周会安排一批自动化测试的进阶练习,编号从1到20,穿插在章节3.1的课程模块里。进阶13、14、15刚好处于一个“理论讲完、开始上手综合场景”的过渡位置。换句话说,前面的练习都在打单点技能,比如定位元素、等待加载、断言文本,而13、14、15开始要求你把多个能力串起来。这也是我把它们当成一个整体项目来做的核心原因:分开做是三道题,合起来就是一个微型自动化测试项目的闭环。

这一篇就围绕“完成进阶13、14、15”的完整过程来写,包含题目拆解、技术选型、实现细节、踩坑记录和最终检验。整个过程的代码量不大,但思路含量不小,对正在学自动化测试、准备从入门跨向进阶的读者而言,参考价值比较直接。

我刚拿到题目时做的第一件事,不是写代码,而是把三道题分别看了一遍,确认每个题目的考查点。把题目分类之后,思路立刻清晰了:

  • 进阶13:重点考显式等待和元素状态判断,场景是一个列表页的异步加载。
  • 进阶14:重点考多个表单控件的组合操作,以及断言时的边界条件。
  • 进阶15:重点考数据驱动,也就是把测试数据和测试逻辑拆开,用一套逻辑跑多组数据。

这三个点正好分别对应自动化测试里的“稳”、“全”、“省”。稳是指脚本不要靠sleep硬等,全是指用例要覆盖正常和异常路径,省是指用数据驱动减少重复代码。把一个任务想成这样的三层结构之后,执行起来就非常顺。

2. 三道进阶题的设计逻辑与核心思路

2.1 进阶13:异步加载场景下的显式等待

这道题给出了一个模拟的订单列表页面,页面加载后需要1到3秒才会把订单数据渲染出来。题目的要求是:编写自动化脚本,打开页面后等待“订单列表加载完成”这个状态出现,然后完成对订单条数和特定订单号的断言。

很多新手看到这种题目,第一反应是用time.sleep(3)。但在一线测试框架里,sleep是纪律性错误,因为它的等待时间不可控。网络快的时候白白等3秒,网络慢的时候3秒可能还不够。正确的姿势是使用显式等待,让脚本轮询某个条件,直到条件满足或者超时。

我在实现时用的是WebDriverWait配合expected_conditions

from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By wait = WebDriverWait(driver, 10) order_items = wait.until( EC.presence_of_all_elements_located((By.CSS_SELECTOR, ".order-item")) )

这里有一个细节值得展开:presence_of_element_located只检查元素是否出现在DOM里,不检查元素是否可见。如果列表项是一个带骨架屏的页面,DOM里可能有占位节点,此时用visibility_of_all_elements_located更稳妥。进阶题里没有明确说明页面是否有骨架屏,但从断言稳定性的角度出发,我直接选择了可见性判断,宁可严格一点也不让脚本出现假阳性。

等待条件选好之后,还有一个隐藏考点:等待超时时间设多少合适。我按常见的3G网络下前端接口响应时间做了估算,取10秒作为兜底超时。这里没有魔法数字,纯粹是实际场景的常见值:一个列表接口在非极端网络下,2到3秒内返回算正常,10秒已经覆盖了大多数慢速情况。如果10秒还没出现,脚本报错也比继续往下执行要好。

2.2 进阶14:多表单控件组合操作与断言

进阶14给的是一个商品筛选页面,筛选条件分别是价格区间输入框、品牌下拉框、销量排序单选按钮,以及一个“查询”按钮。题目要求组合这些控件完成一次筛选,然后验证结果列表里的商品价格都落在指定区间内。

这个题目的难点不在操作控件本身,而在两个容易被忽略的地方:第一,下拉框选项的加载可能是异步的;第二,筛选后的结果列表需要在断言前完成刷新。

对于下拉框,我使用了Select类,这一步本身没有悬念:

from selenium.webdriver.support.ui import Select brand_select = Select(driver.find_element(By.ID, "brand")) brand_select.select_by_visible_text("品牌A")

这里要说一下为什么用select_by_visible_text而不是select_by_value。在真实项目里,下拉框的value属性经常是后端定义的ID,可读性很差,而且不同环境可能不一致。而页面展示的文本是产品经理确认过的,用文本选择更贴近用户视角,脚本维护起来也更直观。

价格区间输入框操作就更直接了,两个send_keys就能搞定。但提交之后的结果校验需要仔细设计断言,因为价格是浮点数,直接比较会碰到精度问题。我的做法是把价格文本转成整数“分”再比较:

def parse_price(price_text): return int(float(price_text.replace("¥", "")) * 100) assert all(low <= parse_price(item.text) <= high for item in price_results)

用“分”做单位,既避开了浮点误差,又符合电商系统里价格计算的基本规则,算是一个实践中比较顺手的小技巧。

2.3 进阶15:数据驱动的参数化改造

进阶15的题目看起来很短:用同一个登录测试流程,验证三组账号密码的组合,分别是正确账号正确密码、正确账号错误密码、不存在账号任意密码。要求代码不复制三份。

这题本质上就是在考数据驱动。我先写了一版最直接的实现,把三组数据放进一个列表,然后通过pytest的参数化装饰器跑同一套逻辑:

import pytest @pytest.mark.parametrize( "username,password,expected", [ ("valid_user", "valid_pass", "登录成功"), ("valid_user", "wrong_pass", "密码错误"), ("ghost_user", "any_pass", "账号不存在"), ], ) def test_login(username, password, expected): login_page.login(username, password) assert expected in login_page.get_message()

这是数据驱动最基础也最清晰的一种形态:数据与逻辑分离,新增一条验证场景只需要往列表里加一行,不用改动测试函数本体。对于一个小型测试项目来说,这种朴素方案已经足够好,不需要一上来就上Excel或YAML。

不过,如果数据量变大,直接把数据堆在装饰器里会让代码文件变得臃肿。进阶15虽然没有要求,我还是顺手把数据抽到了独立的测试数据模块里,这样更符合“数据驱动”的本意:数据和测试逻辑分离开,测试脚本只负责流程和断言。后面如果产品说要加10组异常场景,我只需要改数据文件,谁碰代码的风险都更小。

3. 实操过程与核心环节实现

3.1 环境确认与项目结构整理

动工之前,我先锁定了环境版本,避免不同版本的库带来兼容性问题。这次的运行环境是:Python 3.10、pytest 7.4、Selenium 4.15,浏览器用的是Chrome 120对应版本的chromedriver。Selenium 4相比老版本最大的变化是内置了相对定位和更完善的等待API,用起来顺手不少。

项目目录我按下面这个结构来组织,测试代码、页面对象、测试数据分开存放:

advanced_practice/ ├── pages/ │ ├── order_list_page.py │ ├── filter_page.py │ └── login_page.py ├── test_data/ │ └── login_data.py ├── tests/ │ ├── test_advanced_13.py │ ├── test_advanced_14.py │ └── test_advanced_15.py └── conftest.py

这个结构看起来简单,但实战中特别有用。把页面操作封装到pages目录,测试用例里就不会出现一大串driver.find_element连招,断言的可读性也会好很多。conftest.py里放的是driver的fixture,每个测试模块直接复用,不会重复启动浏览器。

fixture这块我额外说一句,因为很多人写着写着就忽略掉了。我用的是函数级fixture,每个用例跑完就退出浏览器,保证用例之间互不干扰。这种做法的代价是执行慢,但换来的是稳定性和隔离性。在小规模练习项目里,稳定大于速度。

3.2 页面对象封装与等待策略落地

页面对象模式是这个项目的骨架。我拿登录页举例,把元素定位和操作动作封装成方法:

class LoginPage: def __init__(self, driver): self.driver = driver self.username_input = (By.ID, "username") self.password_input = (By.ID, "password") self.submit_button = (By.ID, "submit") self.message = (By.CLASS_NAME, "message") 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.submit_button).click() message_el = WebDriverWait(self.driver, 5).until( EC.visibility_of_element_located(self.message) ) return message_el.text

这里值得注意的点是:我在login方法内部做了显式等待,等待提示消息可见之后才返回文本。这样做的好处是调用方不需要再关心页面什么时候加载完成,拿到返回值就可以直接断言。封装的意义就在于此:把复杂的等待逻辑留在内部,对外暴露一个简单的动作接口。

三个页面对象做完之后,测试用例本身的代码量就很少了。这也是一个经验之谈:页面对象如果没有把等待逻辑封装进去,测试用例就会到处散落WebDriverWait,维护成本很高。宁可把等待写在页面对象里,让它变成一种默认行为,也不要指望每个写用例的人都记得加等待。

3.3 三组用例的执行结果与稳定性观察

代码写完之后,我连续跑了三次测试套件,确认结果稳定。三次执行全部通过,平均耗时在18秒左右,主要时间花在浏览器启动和页面加载上。这个执行速度对10个用例左右的小规模练习项目来说是正常的。

我对结果做了个简单统计:

用例执行时间结果
test_advanced_134.2秒通过
test_advanced_146.8秒通过
test_advanced_157.1秒通过

三次跑完,没有出现偶发失败,说明等待策略和数据驱动设计在稳定性方面是合格的。不过我也清楚,一次通过不代表就没有问题。自动化测试脚本最害怕的不是“跑不过”,而是“这次过、下次挂”的脆性。所以我又多跑了几轮,并且在其中一轮里手动加入了延迟模拟,确保显式等待不是靠运气过关。

3.4 待办事项与扩展空间

做完三道题之后,我给自己列了一个简单的后续清单。第一个待办是给进阶14的筛选场景增加一个“无结果”的测试数据,验证空状态页面是否也能正常断言。第二个待办是把测试报告接入pytest-html,方便把结果分享给团队。第三个待办是重新审视一下CSS选择器,把稳定性不足的class依赖改成更健壮的定位方式。

这些不是题目要求的内容,但对于一个想持续精进的测试工程师来说,做完题只是起点,把练习当成小项目去延伸才是真正的进阶。这也是我在这一节里想传达的核心观点:练习题永远是一个最小可行样本,真正的能力来自你对这个样本的延伸思考。

4. 常见问题与排查技巧实录

4.1 等待时间到了但元素仍然没出现

这是我第一次跑进阶13时碰到的问题。当时我把等待条件设为presence_of_element_located,结果在列表项渲染前,页面已经有一个空的占位DOM节点,脚本直接判定元素存在,后续取订单号时自然就报了空指针。

排查方法很简单,打开浏览器开发者工具,手动刷新页面观察DOM结构的变化。如果发现页面先插入空的div,再用异步请求填充数据,就不能用presence,必须改成visibility或者自定义等待条件。这个排查思路在大多数异步渲染场景里都适用。

4.2 下拉框选项加载比预期慢

进阶14的下拉框也给我上了一课。我以为Select对象创建之后就能直接操作,结果在跑的时候偶尔会报“选项不可见”的错误。仔细看才发现,下拉框的选项是页面加载后通过接口动态插入的,Select初始化时选项还没填充进来。

解决办法是在操作下拉框之前,先用显式等待确保选项数量大于1:

wait.until( lambda d: len(Select(d.find_element(By.ID, "brand")).options) > 1 )

这个lambda表达式看起来有点绕,但它解决的问题很实际:把“下拉框选项已经加载完毕”变成一个可轮询的条件,等条件成立再往下执行。类似这种动态加载在真实项目里比比皆是,养成“操作前先确认状态”的习惯,能省下大量排查时间。

4.3 用例之间互相干扰导致偶发失败

跑进阶15时出现过一次让我困惑的现象:单独跑登录用例全部通过,但把三个用例放在一起跑,第二个用例偶尔失败。后来发现是浏览器缓存引起的。第一个用例登录成功后,第二个用例打开登录页时,页面自动带出了上次输入的账号。

解决这个问题我用了两个层面的手段:第一,在fixture中每次启动浏览器时用一个干净的user data directory;第二,在页面对象里增加了一个清空输入框的操作。两个手段叠加之后,用例之间的隔离性明显提升。这个坑在真实项目中尤其常见,尤其是涉及到登录态、缓存、本地存储的场景,用例隔离做得不好,脚本就是一颗定时炸弹。

5. 从三道题到一套测试习惯的沉淀

把进阶13、14、15做完,回头看这三道题,我最大的体会是:它们不是三个孤立的小练习,而是一条“稳定性、覆盖面、可维护性”的进阶路径。进阶13让你学会等待真实的状态而不是盲等,进阶14让你意识到组合操作的每一步都不能假设页面是静止的,进阶15则是用一个简单的参数化技巧把代码量砍掉三分之二。

在实际项目中,我见过太多返回“测试通过”但实际什么也没验证的脚本,也见过满屏sleep(5)的“稳定”脚本——它们看起来稳,实际上慢且脆弱。真正有价值的自动化测试,应该是又快又稳又有覆盖面的,而这三个维度恰好就是这三道进阶题在训练的东西。

最后分享一个我在这个过程中养成的小习惯:每道题做完之后,不管多简单,都顺手写一段“如果这里是真实项目,我还会补充什么验证”的备注。这种微小的练习做多了,你会发现自己看问题的视角不再局限于“让脚本跑通”,而是慢慢转向“让脚本真的有用”。这种思维转变,比多会几个API要重要得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询