从 0 到 1 搭建 Selenium + Pytest + Page Object 自动化测试框架实战(二):购物车页面与跨页面业务流测试

从 0 到 1 搭建 Selenium + Pytest + Page Object 自动化测试框架实战(二):购物车页面与跨页面业务流测试
上一篇完成了商品列表页的 Page Object 封装与基础用例设计。本篇继续推进 SauceDemo 自动化测试项目目标是完成购物车页面的自动化覆盖并解决商品页与购物车页之间的数据传递、状态校验和业务流程复用问题。上一篇从 0 到 1 搭建 Selenium Pytest Page Object 自动化测试框架实战一商品列表页自动化测试设计测试网站https://www.saucedemo.com/一、本阶段目标与成果如果只验证“点击购物车图标后 URL 是否正确”购物车测试其实很快就能写完。但真实的购物流程不只包含页面跳转还需要验证加入购物车的商品是否正确、名称和价格是否一致、删除后状态是否恢复、刷新后数据是否保留以及是否能够继续进入结算页面。因此本阶段的重点从“单页面操作”进一步推进到了“跨页面业务闭环”。模块本阶段实现内容当前状态BasePage增加无等待的多元素查询支持购物车为空等正常状态已完成InventoryPage获取商品名称与价格、等待购物车数量及按钮文字变化已完成CartPage查询、删除、清空商品继续购物与进入结算已完成InventoryService组合“获取商品信息 → 加入购物车 → 进入购物车”业务流程已完成test_cart.py覆盖数据一致性、删除、状态切换、刷新和结算跳转8 条用例通过Checkout 页面用户信息填写、订单汇总与完成订单下一阶段为了方便读者复现本次项目运行环境如下环境版本或配置Python3.11.2Selenium4.45.0Pytest8.3.5测试网站SauceDemo批量执行模式Chrome Headless二、项目结构升级从 Page 到 Service1. 当前目录结构本阶段新增了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_pageInventoryPage(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):nameself.inventory_page.get_product_name_by_index(index)priceself.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):badgesself.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):elementsself.find_elements(self.item_names)return[el.text.strip()forelinelements]该方法返回一个纯字符串列表测试层不需要直接处理WebElement断言会更加直观。2.3 按商品名称删除而不是依赖固定索引defremove_product_by_name(self,target_name):namesself.find_elements(self.item_names)buttonsself.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:buttonsself.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删除全部商品购物车商品数量为 0CART-05添加后再删除商品角标数量与按钮文字正确切换CART-06返回商品页后再次进入购物车已添加商品仍然保留CART-07非空购物车点击 Checkout进入checkout-step-one页面CART-08刷新购物车页面商品数据保持不变这 8 条用例不仅验证页面能否操作还覆盖了数据一致性、状态变化和页面间的数据持久性。五、关键业务场景验证1. 验证商品名称与价格一致deftest_cart_product_consistency(driver):login(driver)inventory_serviceInventoryService(driver)cart_pageCartPage(driver)expected_name,expected_price(inventory_service.prepare_single_product_cart(3))cart_indexcart_page.get_product_index_by_name(expected_name)actual_namecart_page.get_product_name_by_index(cart_index)actual_pricecart_page.get_product_price_by_index(cart_index)assertactual_nameexpected_nameassertactual_priceexpected_price该用例没有直接写死商品名称和价格而是在商品页动态获取预期数据再与购物车页的实际数据进行比较。这样做的好处是即使测试网站以后调整商品价格只要商品页和购物车页保持一致用例仍然能够正确验证业务逻辑。2. 验证添加与删除后的状态闭环购物车测试不能只关注购物车页面还要确认返回商品列表页后按钮和角标是否恢复。deftest_add_and_remove_product_state_toggle(driver):login(driver)inventory_pageInventoryPage(driver)cart_pageCartPage(driver)index0initial_cart_countinventory_page.get_cart_count()assertinitial_cart_count0inventory_page.add_product_by_index(index)button_textinventory_page.wait_for_product_button_text_by_index(index,Remove,).lower()assertremoveinbutton_textassertinventory_page.get_cart_count()initial_cart_count1target_nameinventory_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_textinventory_page.wait_for_product_button_text_by_index(index,Add to cart,).lower()assertadd to cartinbutton_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):expectedexpected_text.strip().lower()defbutton_text_matches(_driver):buttonsself.find_elements_no_wait(self.add_to_cart_buttons)ifindex0orindexlen(buttons):returnFalsetry:actual_textbuttons[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_argsconfig.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)图 4Pytest 执行结果截图至此当前购物车页面的 8 条自动化测试全部通过。八、阶段复盘与下一步计划本阶段最重要的收获并不是多写了一个CartPage而是让项目开始具备跨页面业务测试的结构。已解决的问题使用无等待查询正确处理“元素不存在也是正常结果”的场景使用商品名称定位目标商品降低删除操作对固定索引的依赖通过 Service 层复用跨页面准备流程使用显式等待替代固定休眠从简单 URL 断言扩展到名称、价格、数量和状态一致性断言完成添加、删除、返回、刷新和结算跳转等购物车核心路径覆盖。仍可继续优化的地方将账号、密码、基础地址等固定数据移入配置文件为测试数据增加 pytest 参数化增加失败截图、日志和 Allure 测试报告对异常用户和边界场景进行补充继续完成 Checkout 信息页、订单概览页和完成订单页的自动化测试。下一阶段计划下一阶段将进入 Checkout 结算流程计划覆盖姓名、邮编等用户信息填写必填项为空时的错误提示订单商品与价格汇总校验商品小计、税费和总价计算完成订单后的成功提示从购物车到订单完成的端到端业务流程。项目也会继续从“能够运行”向“稳定、可维护、可扩展”推进。如果你也在学习 Selenium、Pytest 和 Page Object Model欢迎一起交流自动化测试中的设计思路与踩坑记录。推荐标签Selenium、Pytest、Python、Page Object Model、UI 自动化测试、软件测试