上一篇完成了商品列表页的 Page Object 封装与基础用例设计。本篇继续推进 SauceDemo 自动化测试项目,目标是完成购物车页面的自动化覆盖,并解决商品页与购物车页之间的数据传递、状态校验和业务流程复用问题。
上一篇:从 0 到 1 搭建 Selenium + Pytest + Page Object 自动化测试框架实战(一):商品列表页自动化测试设计
测试网站:https://www.saucedemo.com/
一、本阶段目标与成果
如果只验证“点击购物车图标后 URL 是否正确”,购物车测试其实很快就能写完。
但真实的购物流程不只包含页面跳转,还需要验证:加入购物车的商品是否正确、名称和价格是否一致、删除后状态是否恢复、刷新后数据是否保留,以及是否能够继续进入结算页面。
因此,本阶段的重点从“单页面操作”进一步推进到了“跨页面业务闭环”。
| 模块 | 本阶段实现内容 | 当前状态 |
|---|---|---|
BasePage | 增加无等待的多元素查询,支持购物车为空等正常状态 | 已完成 |
InventoryPage | 获取商品名称与价格、等待购物车数量及按钮文字变化 | 已完成 |
CartPage | 查询、删除、清空商品,继续购物与进入结算 | 已完成 |
InventoryService | 组合“获取商品信息 → 加入购物车 → 进入购物车”业务流程 | 已完成 |
test_cart.py | 覆盖数据一致性、删除、状态切换、刷新和结算跳转 | 8 条用例通过 |
| Checkout 页面 | 用户信息填写、订单汇总与完成订单 | 下一阶段 |
为了方便读者复现,本次项目运行环境如下:
| 环境 | 版本或配置 |
|---|---|
| Python | 3.11.2 |
| Selenium | 4.45.0 |
| Pytest | 8.3.5 |
| 测试网站 | SauceDemo |
| 批量执行模式 | Chrome Headless |
二、项目结构升级:从 Page 到 Service
1. 当前目录结构
本阶段新增了cart_page.py和inventory_service.py,项目不再只有 Base 层、Page 层和测试层,而是开始引入业务流程层。
SWAG_Lags_Autotest/ ├── base/ │ └── base_page.py ├── pages/ │ ├── login_page.py │ ├── inventory_page.py │ └── cart_page.py ├── services/ │ └── inventory_service.py ├── testcases/ │ ├── test_login.py │ ├── test_inventory.py │ └── test_cart.py ├── utils/ │ └── utils.py └── conftest.py各层之间的调用关系如下:
test_cart.py │ ├── 调用 InventoryService 组织跨页面流程 │ └── 调用 InventoryPage 获取商品信息并加入购物车 │ ├── 调用 CartPage 完成购物车页面操作 │ └── 使用 assert 校验最终业务结果 InventoryPage / CartPage └── 继承 BasePage,复用 Selenium 基础能力这里需要注意:Service 层并不是为了增加项目复杂度,而是为了存放需要组合多个页面动作的业务流程。单次点击、读取文字等操作仍然由 Page 层负责。
2. InventoryService:封装跨页面业务流程
最初的测试代码通常会直接写成:
读取商品名称 → 读取商品价格 → 点击 Add to cart → 点击购物车图标当多个用例都需要这段准备流程时,重复代码会越来越多。因此新增InventoryService,专门组合跨页面动作。
frompages.inventory_pageimportInventoryPageclassInventoryService:def__init__(self,driver):self.inventory_page=InventoryPage(driver)defadd_product_and_go_to_cart(self,index):self.inventory_page.add_product_by_index(index)self.inventory_page.go_to_cart()defprepare_single_product_cart(self,index):name=self.inventory_page.get_product_name_by_index(index)price=self.inventory_page.get_product_price_by_index(index)self.add_product_and_go_to_cart(index)returnname,priceprepare_single_product_cart()在离开商品页之前先保存预期名称和价格,然后完成加入购物车与页面跳转,最后把预期数据交给测试层。
三层职责因此变得清晰:
| 层级 | 关注点 | 示例 |
|---|---|---|
| Page 层 | 页面有什么元素、能够执行什么操作 | 点击按钮、读取名称、删除商品 |
| Service 层 | 多个页面动作如何组成业务流程 | 获取商品信息并进入购物车 |
| Test 层 | 预期结果是否成立 | 名称、价格、数量和状态断言 |
图 1 项目分层与调用关系
三、购物车页面对象设计
图 2 SauceDemo 购物车页面核心元素
1. 使用无等待查询处理购物车空状态
在商品列表页中,购物车角标只有在购物车存在商品时才会显示;在购物车页面中,删除全部商品后,.cart_item元素也会直接从 DOM 中消失。
如果继续使用带显式等待的find_elements(),程序会把“元素本来就应该不存在”的正常状态当成异常,并等待到超时。
因此在BasePage中增加一个允许返回空列表的方法:
deffind_elements_no_wait(self,locator):"""不等待,直接返回元素列表,允许结果为空。"""returnself.driver.find_elements(*locator)这样,获取购物车数量时就可以自然地处理空状态:
defget_cart_count(self):badges=self.find_elements_no_wait(self.cart_badge)ifnotbadges:return0returnint(badges[0].text)这个改动看起来很小,但它体现了一个重要原则:
显式等待适合“元素最终应该出现”的场景;无等待查询适合“元素不存在也是合法结果”的场景。
2. CartPage:只描述购物车页面能够做什么
购物车页面主要包含商品列表、商品名称、价格、删除按钮、继续购物按钮和结算按钮。
2.1 核心元素定位
frombase.base_pageimportBasePagefromselenium.webdriver.common.byimportByclassCartPage(BasePage):cart_items=(By.CLASS_NAME,"cart_item")item_names=(By.CSS_SELECTOR,".inventory_item_name")item_prices=(By.CSS_SELECTOR,".inventory_item_price")remove_buttons=(By.CSS_SELECTOR,".cart_item button")checkout_button=(By.ID,"checkout")continue_shopping_button=(By.ID,"continue-shopping")定位器统一放在页面对象中。后续如果页面属性发生变化,只需要修改CartPage,测试用例不需要到处调整定位代码。
2.2 获取购物车中的所有商品名称
defget_all_product_names(self):elements=self.find_elements(self.item_names)return[el.text.strip()forelinelements]该方法返回一个纯字符串列表,测试层不需要直接处理WebElement,断言会更加直观。
2.3 按商品名称删除,而不是依赖固定索引
defremove_product_by_name(self,target_name):names=self.find_elements(self.item_names)buttons=self.find_elements(self.remove_buttons)forindex,elementinenumerate(names):ifelement.text.strip()==target_name:buttons[index].click()returnraiseException(f"购物车中未找到商品:{target_name}")索引适合操作固定位置的商品,但购物车经过删除后,剩余商品的索引会发生变化。测试“删除指定商品”时,使用商品名称作为业务标识更稳定,也更符合真实测试场景。
2.4 清空购物车时重新获取按钮
defremove_all_products(self):whileTrue:buttons=self.find_elements_no_wait(self.remove_buttons)ifnotbuttons:breakself.driver.execute_script("arguments[0].click();",buttons[0],)每删除一个商品,页面 DOM 都会发生变化,之前保存的按钮元素可能失效。因此循环中需要重新查询按钮,而不是在循环外一次性获取全部元素。
四、购物车自动化测试用例设计
本阶段共编写 8 条购物车测试用例:
| 编号 | 测试场景 | 核心断言 |
|---|---|---|
| CART-01 | 单个商品加入购物车 | 名称和价格与商品页一致 |
| CART-02 | 多个商品加入购物车 | 实际商品集合与预期集合一致 |
| CART-03 | 删除指定商品 | 目标商品不再出现在购物车中 |
| CART-04 | 删除全部商品 | 购物车商品数量为 0 |
| CART-05 | 添加后再删除商品 | 角标数量与按钮文字正确切换 |
| CART-06 | 返回商品页后再次进入购物车 | 已添加商品仍然保留 |
| CART-07 | 非空购物车点击 Checkout | 进入checkout-step-one页面 |
| CART-08 | 刷新购物车页面 | 商品数据保持不变 |
这 8 条用例不仅验证页面能否操作,还覆盖了数据一致性、状态变化和页面间的数据持久性。
五、关键业务场景验证
1. 验证商品名称与价格一致
deftest_cart_product_consistency(driver):login(driver)inventory_service=InventoryService(driver)cart_page=CartPage(driver)expected_name,expected_price=(inventory_service.prepare_single_product_cart(3))cart_index=cart_page.get_product_index_by_name(expected_name)actual_name=cart_page.get_product_name_by_index(cart_index)actual_price=cart_page.get_product_price_by_index(cart_index)assertactual_name==expected_nameassertactual_price==expected_price该用例没有直接写死商品名称和价格,而是在商品页动态获取预期数据,再与购物车页的实际数据进行比较。
这样做的好处是:即使测试网站以后调整商品价格,只要商品页和购物车页保持一致,用例仍然能够正确验证业务逻辑。
2. 验证添加与删除后的状态闭环
购物车测试不能只关注购物车页面,还要确认返回商品列表页后,按钮和角标是否恢复。
deftest_add_and_remove_product_state_toggle(driver):login(driver)inventory_page=InventoryPage(driver)cart_page=CartPage(driver)index=0initial_cart_count=inventory_page.get_cart_count()assertinitial_cart_count==0inventory_page.add_product_by_index(index)button_text=inventory_page.wait_for_product_button_text_by_index(index,"Remove",).lower()assert"remove"inbutton_textassertinventory_page.get_cart_count()==initial_cart_count+1target_name=inventory_page.get_product_name_by_index(index)inventory_page.go_to_cart()cart_page.remove_product_by_name(target_name)cart_page.go_back_to_inventory()button_text=inventory_page.wait_for_product_button_text_by_index(index,"Add to cart",).lower()assert"add to cart"inbutton_textassertinventory_page.get_cart_count()==initial_cart_count这条用例形成了完整闭环:
购物车为空 ↓ 添加商品 ↓ 按钮变为 Remove,角标数量 +1 ↓ 进入购物车并删除商品 ↓ 返回商品页 ↓ 按钮恢复 Add to cart,角标数量恢复图 3 添加与删除后的页面状态闭环
六、稳定性与执行策略优化
1. 用显式等待替代固定休眠
在前一阶段的学习代码中,部分操作依赖time.sleep()。固定休眠虽然直观,但页面可能提前完成,也可能在网络较慢时仍未完成,既浪费时间又不稳定。
本阶段根据真实状态进行等待:
defwait_for_cart_count(self,expected_count):WebDriverWait(self.driver,self.TIMEOUT).until(lambda_driver:self.get_cart_count()==expected_count)returnself.get_cart_count()按钮文字变化也采用同样思路,并额外处理 DOM 更新时可能出现的StaleElementReferenceException。
defwait_for_product_button_text_by_index(self,index,expected_text):expected=expected_text.strip().lower()defbutton_text_matches(_driver):buttons=self.find_elements_no_wait(self.add_to_cart_buttons)ifindex<0orindex>=len(buttons):returnFalsetry:actual_text=buttons[index].text.strip()exceptStaleElementReferenceException:returnFalseifactual_text.lower()==expected:returnactual_textreturnFalsereturnWebDriverWait(self.driver,self.TIMEOUT).until(button_text_matches)等待的对象从“固定秒数”变成了“业务状态已经满足”,测试稳定性和执行效率都会更合理。
2. Fixture 的运行模式优化
为了兼顾单条用例调试和批量执行效率,conftest.py会根据 pytest 的执行目标切换浏览器模式:
- 直接指定某一条用例:使用可视化浏览器,方便观察和调试;
- 执行整个测试文件或全部用例:使用无头模式,减少界面干扰。
defis_single_test_target(config:pytest.Config)->bool:invocation_args=config.invocation_params.argsreturnany("::"instr(argument)forargumentininvocation_args)这种方式不需要在代码中手动修改headless配置,只需要改变 pytest 命令即可。
七、测试执行与结果分析
执行购物车测试文件:
python-mpytest testcases/test_cart.py-q本机实际执行结果:
........ [100%] 8 passed in 116.76s (0:01:56)图 4:Pytest 执行结果截图
至此,当前购物车页面的 8 条自动化测试全部通过。
八、阶段复盘与下一步计划
本阶段最重要的收获并不是多写了一个CartPage,而是让项目开始具备跨页面业务测试的结构。
已解决的问题
- 使用无等待查询正确处理“元素不存在也是正常结果”的场景;
- 使用商品名称定位目标商品,降低删除操作对固定索引的依赖;
- 通过 Service 层复用跨页面准备流程;
- 使用显式等待替代固定休眠;
- 从简单 URL 断言扩展到名称、价格、数量和状态一致性断言;
- 完成添加、删除、返回、刷新和结算跳转等购物车核心路径覆盖。
仍可继续优化的地方
- 将账号、密码、基础地址等固定数据移入配置文件;
- 为测试数据增加 pytest 参数化;
- 增加失败截图、日志和 Allure 测试报告;
- 对异常用户和边界场景进行补充;
- 继续完成 Checkout 信息页、订单概览页和完成订单页的自动化测试。
下一阶段计划
下一阶段将进入 Checkout 结算流程,计划覆盖:
- 姓名、邮编等用户信息填写;
- 必填项为空时的错误提示;
- 订单商品与价格汇总校验;
- 商品小计、税费和总价计算;
- 完成订单后的成功提示;
- 从购物车到订单完成的端到端业务流程。
项目也会继续从“能够运行”向“稳定、可维护、可扩展”推进。
如果你也在学习 Selenium、Pytest 和 Page Object Model,欢迎一起交流自动化测试中的设计思路与踩坑记录。
推荐标签:Selenium、Pytest、Python、Page Object Model、UI 自动化测试、软件测试