Selenium 实战项目:电子商务网站自动化测试
Selenium 实战项目 | 菜鸟 ,建议你先看完前面这篇再来看下面这篇。
下面我将带你完成一个完整的 Selenium 实战项目,模拟用户在一个电子商务网站上的完整购物流程。这个项目将综合运用我们之前学到的所有知识。
项目概述
我们将创建一个自动化测试脚本,模拟用户在电子商务网站上完成以下操作:
-
访问网站首页
-
用户登录
-
浏览商品分类
-
搜索商品
-
将商品添加到购物车
-
查看购物车
-
进入结算流程
-
填写配送信息
-
选择支付方式
-
完成订单
项目结构
ecommerce-automation/
├── pages/ # 页面对象类
│ ├── __init__.py
│ ├── base_page.py # 基础页面类
│ ├── home_page.py # 首页
│ ├── login_page.py # 登录页
│ ├── product_page.py # 商品页
│ ├── cart_page.py # 购物车页
│ └── checkout_page.py # 结算页
├── tests/ # 测试用例
│ ├── __init__.py
│ └── test_shopping_flow.py # 主要测试流程
├── utils/ # 工具类
│ ├── __init__.py
│ ├── config.py # 配置文件
│ └── helpers.py # 辅助函数
├── reports/ # 测试报告
├── screenshots/ # 测试截图
├── requirements.txt # 项目依赖
└── run_tests.py # 测试运行入口
环境准备
首先创建 requirements.txt
文件:
selenium==4.15.0
pytest==7.4.3
pytest-html==4.1.1
pytest-xdist==3.5.0
allure-pytest==2.13.2
webdriver-manager==4.0.1
安装依赖:
pip install -r requirements.txt
配置文件
创建 utils/config.py
:
import os
from datetime import datetimeclass Config:# 浏览器配置BROWSER = "chrome" # chrome, firefox, edgeHEADLESS = TrueWINDOW_SIZE = "1920,1080"IMPLICIT_WAIT = 10# 测试网站URLBASE_URL = "https://www.saucedemo.com/"# 测试用户凭证STANDARD_USER = "standard_user"LOCKED_USER = "locked_out_user"PROBLEM_USER = "problem_user"PERFORMANCE_USER = "performance_glitch_user"PASSWORD = "secret_sauce"# 路径配置PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))SCREENSHOT_DIR = os.path.join(PROJECT_ROOT, "screenshots")REPORT_DIR = os.path.join(PROJECT_ROOT, "reports")# 测试数据TEST_PRODUCT = "Sauce Labs Backpack"TEST_FIRST_NAME = "Test"TEST_LAST_NAME = "User"TEST_ZIP_CODE = "12345"@staticmethoddef get_timestamp():return datetime.now().strftime("%Y%m%d_%H%M%S")@staticmethoddef setup_directories():os.makedirs(Config.SCREENSHOT_DIR, exist_ok=True)os.makedirs(Config.REPORT_DIR, exist_ok=True)# 初始化目录
Config.setup_directories()
基础页面类
创建 pages/base_page.py
:
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException, NoSuchElementException
from utils.config import Config
import logging
import allurelogger = logging.getLogger(__name__)class BasePage:def __init__(self, driver):self.driver = driverself.wait = WebDriverWait(driver, Config.IMPLICIT_WAIT)def find_element(self, locator):"""查找元素,带有显式等待"""try:return self.wait.until(EC.visibility_of_element_located(locator))except TimeoutException:logger.error(f"元素未找到: {locator}")raisedef find_elements(self, locator):"""查找多个元素"""try:return self.wait.until(EC.visibility_of_all_elements_located(locator))except TimeoutException:logger.error(f"元素未找到: {locator}")return []def click(self, locator):"""点击元素"""element = self.find_element(locator)try:element.click()logger.info(f"点击元素: {locator}")except Exception as e:logger.error(f"点击元素失败: {locator}, 错误: {e}")raisedef input_text(self, locator, text):"""输入文本"""element = self.find_element(locator)try:element.clear()element.send_keys(text)logger.info(f"在元素 {locator} 中输入文本: {text}")except Exception as e:logger.error(f"输入文本失败: {locator}, 错误: {e}")raisedef get_text(self, locator):"""获取元素文本"""element = self.find_element(locator)return element.textdef is_element_present(self, locator):"""检查元素是否存在"""try:self.find_element(locator)return Trueexcept (TimeoutException, NoSuchElementException):return Falsedef take_screenshot(self, name):"""截图并附加到Allure报告"""screenshot_path = f"{Config.SCREENSHOT_DIR}/{name}_{Config.get_timestamp()}.png"self.driver.save_screenshot(screenshot_path)allure.attach(self.driver.get_screenshot_as_png(),name=name,attachment_type=allure.attachment_type.PNG)return screenshot_pathdef wait_for_page_load(self):"""等待页面加载完成"""self.wait.until(lambda driver: driver.execute_script("return document.readyState") == "complete")
页面对象类
创建 pages/home_page.py
:
from selenium.webdriver.common.by import By
from .base_page import BasePageclass HomePage(BasePage):# 定位器LOGO = (By.CLASS_NAME, "app_logo")BURGER_MENU = (By.ID, "react-burger-menu-btn")SHOPPING_CART = (By.CLASS_NAME, "shopping_cart_link")PRODUCTS_TITLE = (By.CLASS_NAME, "title")PRODUCT_ITEMS = (By.CLASS_NAME, "inventory_item")PRODUCT_NAMES = (By.CLASS_NAME, "inventory_item_name")ADD_TO_CART_BUTTONS = (By.XPATH, "//button[contains(text(), 'Add to cart')]")REMOVE_BUTTONS = (By.XPATH, "//button[contains(text(), 'Remove')]")SORT_DROPDOWN = (By.CLASS_NAME, "product_sort_container")def __init__(self, driver):super().__init__(driver)def is_home_page_loaded(self):"""检查首页是否加载完成"""return self.is_element_present(self.PRODUCTS_TITLE)def get_product_count(self):"""获取商品数量"""return len(self.find_elements(self.PRODUCT_ITEMS))def get_product_names(self):"""获取所有商品名称"""return [product.text for product in self.find_elements(self.PRODUCT_NAMES)]def add_product_to_cart(self, product_name):"""添加指定商品到购物车"""products = self.find_elements(self.PRODUCT_NAMES)add_buttons = self.find_elements(self.ADD_TO_CART_BUTTONS)for i, product in enumerate(products):if product.text == product_name:add_buttons[i].click()return Truereturn Falsedef go_to_cart(self):"""前往购物车"""self.click(self.SHOPPING_CART)from .cart_page import CartPagereturn CartPage(self.driver)def sort_products(self, sort_by):"""排序商品"""from selenium.webdriver.support.ui import Selectdropdown = Select(self.find_element(self.SORT_DROPDOWN))dropdown.select_by_visible_text(sort_by)
创建 pages/login_page.py
:
from selenium.webdriver.common.by import By
from .base_page import BasePage
from utils.config import Configclass LoginPage(BasePage):# 定位器USERNAME_FIELD = (By.ID, "user-name")PASSWORD_FIELD = (By.ID, "password")LOGIN_BUTTON = (By.ID, "login-button")ERROR_MESSAGE = (By.CSS_SELECTOR, "[data-test='error']")def __init__(self, driver):super().__init__(driver)self.driver.get(Config.BASE_URL)def login(self, username, password):"""登录操作"""self.input_text(self.USERNAME_FIELD, username)self.input_text(self.PASSWORD_FIELD, password)self.click(self.LOGIN_BUTTON)# 返回首页对象from .home_page import HomePagereturn HomePage(self.driver)def get_error_message(self):"""获取错误消息"""if self.is_element_present(self.ERROR_MESSAGE):return self.get_text(self.ERROR_MESSAGE)return Nonedef is_login_page_loaded(self):"""检查登录页是否加载完成"""return self.is_element_present(self.LOGIN_BUTTON)
创建 pages/product_page.py
:
from selenium.webdriver.common.by import By
from .base_page import BasePageclass ProductPage(BasePage):# 定位器PRODUCT_NAME = (By.CLASS_NAME, "inventory_details_name")PRODUCT_DESCRIPTION = (By.CLASS_NAME, "inventory_details_desc")PRODUCT_PRICE = (By.CLASS_NAME, "inventory_details_price")ADD_TO_CART_BUTTON = (By.XPATH, "//button[contains(text(), 'Add to cart')]")REMOVE_BUTTON = (By.XPATH, "//button[contains(text(), 'Remove')]")BACK_BUTTON = (By.ID, "back-to-products")def __init__(self, driver):super().__init__(driver)def get_product_name(self):"""获取商品名称"""return self.get_text(self.PRODUCT_NAME)def get_product_price(self):"""获取商品价格"""return self.get_text(self.PRODUCT_PRICE)def add_to_cart(self):"""添加到购物车"""self.click(self.ADD_TO_CART_BUTTON)def remove_from_cart(self):"""从购物车移除"""self.click(self.REMOVE_BUTTON)def back_to_products(self):"""返回商品列表"""self.click(self.BACK_BUTTON)from .home_page import HomePagereturn HomePage(self.driver)
创建 pages/cart_page.py
:
from selenium.webdriver.common.by import By
from .base_page import BasePageclass CartPage(BasePage):# 定位器CART_ITEMS = (By.CLASS_NAME, "cart_item")ITEM_NAMES = (By.CLASS_NAME, "inventory_item_name")ITEM_PRICES = (By.CLASS_NAME, "inventory_item_price")REMOVE_BUTTONS = (By.XPATH, "//button[contains(text(), 'Remove')]")CONTINUE_SHOPPING_BUTTON = (By.ID, "continue-shopping")CHECKOUT_BUTTON = (By.ID, "checkout")def __init__(self, driver):super().__init__(driver)def get_cart_items_count(self):"""获取购物车中商品数量"""return len(self.find_elements(self.CART_ITEMS))def get_item_names(self):"""获取购物车中所有商品名称"""return [item.text for item in self.find_elements(self.ITEM_NAMES)]def get_item_prices(self):"""获取购物车中所有商品价格"""return [price.text for price in self.find_elements(self.ITEM_PRICES)]def remove_item(self, item_name):"""移除指定商品"""items = self.find_elements(self.ITEM_NAMES)remove_buttons = self.find_elements(self.REMOVE_BUTTONS)for i, item in enumerate(items):if item.text == item_name:remove_buttons[i].click()return Truereturn Falsedef continue_shopping(self):"""继续购物"""self.click(self.CONTINUE_SHOPPING_BUTTON)from .home_page import HomePagereturn HomePage(self.driver)def checkout(self):"""结算"""self.click(self.CHECKOUT_BUTTON)from .checkout_page import CheckoutPagereturn CheckoutPage(self.driver)
创建 pages/checkout_page.py
:
from selenium.webdriver.common.by import By
from .base_page import BasePage
from utils.config import Configclass CheckoutPage(BasePage):# 定位器FIRST_NAME_FIELD = (By.ID, "first-name")LAST_NAME_FIELD = (By.ID, "last-name")ZIP_CODE_FIELD = (By.ID, "postal-code")CONTINUE_BUTTON = (By.ID, "continue")CANCEL_BUTTON = (By.ID, "cancel")ERROR_MESSAGE = (By.CSS_SELECTOR, "[data-test='error']")# 第二步定位器ITEM_TOTAL = (By.CLASS_NAME, "summary_subtotal_label")TAX = (By.CLASS_NAME, "summary_tax_label")TOTAL = (By.CLASS_NAME, "summary_total_label")FINISH_BUTTON = (By.ID, "finish")# 完成页定位器COMPLETE_HEADER = (By.CLASS_NAME, "complete-header")COMPLETE_TEXT = (By.CLASS_NAME, "complete-text")BACK_HOME_BUTTON = (By.ID, "back-to-products")def __init__(self, driver):super().__init__(driver)def fill_shipping_info(self, first_name=None, last_name=None, zip_code=None):"""填写配送信息"""first_name = first_name or Config.TEST_FIRST_NAMElast_name = last_name or Config.TEST_LAST_NAMEzip_code = zip_code or Config.TEST_ZIP_CODEself.input_text(self.FIRST_NAME_FIELD, first_name)self.input_text(self.LAST_NAME_FIELD, last_name)self.input_text(self.ZIP_CODE_FIELD, zip_code)def continue_to_overview(self):"""继续到订单概览"""self.click(self.CONTINUE_BUTTON)def cancel_checkout(self):"""取消结算"""self.click(self.CANCEL_BUTTON)from .cart_page import CartPagereturn CartPage(self.driver)def get_error_message(self):"""获取错误消息"""if self.is_element_present(self.ERROR_MESSAGE):return self.get_text(self.ERROR_MESSAGE)return Nonedef get_item_total(self):"""获取商品总额"""return self.get_text(self.ITEM_TOTAL)def get_tax(self):"""获取税费"""return self.get_text(self.TAX)def get_total(self):"""获取总计"""return self.get_text(self.TOTAL)def finish_order(self):"""完成订单"""self.click(self.FINISH_BUTTON)def is_order_complete(self):"""检查订单是否完成"""return self.is_element_present(self.COMPLETE_HEADER)def get_complete_message(self):"""获取完成消息"""return self.get_text(self.COMPLETE_HEADER)def back_to_home(self):"""返回首页"""self.click(self.BACK_HOME_BUTTON)from .home_page import HomePagereturn HomePage(self.driver)
辅助工具类
创建 utils/helpers.py
:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.firefox.options import Options as FirefoxOptions
from selenium.webdriver.edge.options import Options as EdgeOptions
from webdriver_manager.chrome import ChromeDriverManager
from webdriver_manager.firefox import GeckoDriverManager
from webdriver_manager.microsoft import EdgeChromiumDriverManager
from utils.config import Config
import logginglogger = logging.getLogger(__name__)def create_driver(browser_name=None, headless=None):"""创建WebDriver实例"""browser_name = browser_name or Config.BROWSERheadless = headless if headless is not None else Config.HEADLESSif browser_name.lower() == "chrome":options = Options()if headless:options.add_argument("--headless=new")options.add_argument("--no-sandbox")options.add_argument("--disable-dev-shm-usage")options.add_argument(f"--window-size={Config.WINDOW_SIZE}")driver = webdriver.Chrome(service=webdriver.chrome.service.Service(ChromeDriverManager().install()),options=options)elif browser_name.lower() == "firefox":options = FirefoxOptions()if headless:options.add_argument("-headless")options.add_argument(f"--width={Config.WINDOW_SIZE.split(',')[0]}")options.add_argument(f"--height={Config.WINDOW_SIZE.split(',')[1]}")driver = webdriver.Firefox(service=webdriver.firefox.service.Service(GeckoDriverManager().install()),options=options)elif browser_name.lower() == "edge":options = EdgeOptions()if headless:options.add_argument("--headless")options.add_argument(f"--window-size={Config.WINDOW_SIZE}")driver = webdriver.Edge(service=webdriver.edge.service.Service(EdgeChromiumDriverManager().install()),options=options)else:raise ValueError(f"不支持的浏览器: {browser_name}")driver.implicitly_wait(Config.IMPLICIT_WAIT)logger.info(f"创建 {browser_name} 浏览器实例,无头模式: {headless}")return driverdef setup_logging():"""配置日志"""logging.basicConfig(level=logging.INFO,format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',handlers=[logging.FileHandler(f"{Config.PROJECT_ROOT}/automation.log"),logging.StreamHandler()])
测试用例
创建 tests/test_shopping_flow.py
:
import pytest
import allure
from utils.helpers import create_driver
from utils.config import Config
from pages.login_page import LoginPage@allure.feature("电子商务购物流程")
@allure.story("完整购物流程测试")
class TestShoppingFlow:@pytest.fixture(autouse=True)def setup(self):"""测试 setup"""self.driver = create_driver()yieldself.driver.quit()@allure.title("测试完整购物流程")@allure.severity(allure.severity_level.CRITICAL)def test_complete_shopping_flow(self):"""测试完整购物流程"""with allure.step("1. 登录网站"):login_page = LoginPage(self.driver)assert login_page.is_login_page_loaded(), "登录页面未正确加载"home_page = login_page.login(Config.STANDARD_USER, Config.PASSWORD)assert home_page.is_home_page_loaded(), "首页未正确加载"home_page.take_screenshot("登录成功")with allure.step("2. 浏览商品"):product_count = home_page.get_product_count()assert product_count > 0, "商品列表为空"product_names = home_page.get_product_names()allure.attach(str(product_names), "商品列表", allure.attachment_type.TEXT)with allure.step("3. 添加商品到购物车"):result = home_page.add_product_to_cart(Config.TEST_PRODUCT)assert result, f"未找到商品: {Config.TEST_PRODUCT}"home_page.take_screenshot("添加商品到购物车")with allure.step("4. 查看购物车"):cart_page = home_page.go_to_cart()assert cart_page.get_cart_items_count() == 1, "购物车商品数量不正确"cart_items = cart_page.get_item_names()assert Config.TEST_PRODUCT in cart_items, "添加的商品不在购物车中"cart_page.take_screenshot("购物车页面")with allure.step("5. 进入结算流程"):checkout_page = cart_page.checkout()checkout_page.take_screenshot("结算页面第一步")with allure.step("6. 填写配送信息"):checkout_page.fill_shipping_info()checkout_page.continue_to_overview()checkout_page.take_screenshot("订单概览")with allure.step("7. 验证订单信息"):item_total = checkout_page.get_item_total()tax = checkout_page.get_tax()total = checkout_page.get_total()allure.attach(f"商品总额: {item_total}\n税费: {tax}\n总计: {total}", "订单金额信息", allure.attachment_type.TEXT)with allure.step("8. 完成订单"):checkout_page.finish_order()assert checkout_page.is_order_complete(), "订单未成功完成"complete_message = checkout_page.get_complete_message()assert "thank you for your order" in complete_message.lower(), "订单完成消息不正确"checkout_page.take_screenshot("订单完成")@allure.title("测试无效登录")@allure.severity(allure.severity_level.NORMAL)def test_invalid_login(self):"""测试无效登录"""with allure.step("使用错误凭证登录"):login_page = LoginPage(self.driver)login_page.login("invalid_user", "wrong_password")error_message = login_page.get_error_message()assert error_message is not None, "未显示错误消息"assert "username and password do not match" in error_message.lower()login_page.take_screenshot("登录错误")@allure.title("测试购物车操作")@allure.severity(allure.severity_level.NORMAL)def test_cart_operations(self):"""测试购物车操作"""with allure.step("登录并添加多个商品"):login_page = LoginPage(self.driver)home_page = login_page.login(Config.STANDARD_USER, Config.PASSWORD)# 添加两个商品home_page.add_product_to_cart(Config.TEST_PRODUCT)home_page.add_product_to_cart("Sauce Labs Bike Light")cart_page = home_page.go_to_cart()assert cart_page.get_cart_items_count() == 2, "购物车商品数量不正确"cart_page.take_screenshot("购物车中有两个商品")with allure.step("从购物车移除一个商品"):cart_page.remove_item(Config.TEST_PRODUCT)assert cart_page.get_cart_items_count() == 1, "商品移除失败"cart_page.take_screenshot("移除一个商品后")with allure.step("继续购物并添加另一个商品"):home_page = cart_page.continue_shopping()home_page.add_product_to_cart("Sauce Labs Bolt T-Shirt")cart_page = home_page.go_to_cart()assert cart_page.get_cart_items_count() == 2, "继续购物后商品数量不正确"cart_page.take_screenshot("继续购物后")
测试运行入口
创建 run_tests.py
:
import pytest
import os
import shutil
from utils.helpers import setup_logging
from utils.config import Configdef run_tests():"""运行测试"""# 设置日志setup_logging()# 清理之前的报告和截图if os.path.exists(Config.REPORT_DIR):shutil.rmtree(Config.REPORT_DIR)if os.path.exists(Config.SCREENSHOT_DIR):shutil.rmtree(Config.SCREENSHOT_DIR)# 重新创建目录Config.setup_directories()# 运行测试pytest_args = ["-v","--tb=short",f"--html={Config.REPORT_DIR}/report.html","--self-contained-html","--alluredir", f"{Config.REPORT_DIR}/allure-results","-n", "2" # 并行运行2个测试]pytest.main(pytest_args)# 生成Allure报告if shutil.which("allure"):os.system(f"allure generate {Config.REPORT_DIR}/allure-results -o {Config.REPORT_DIR}/allure-report --clean")print(f"Allure报告已生成: {Config.REPORT_DIR}/allure-report/index.html")print(f"HTML报告已生成: {Config.REPORT_DIR}/report.html")if __name__ == "__main__":run_tests()
运行测试
在项目根目录下运行:
python run_tests.py
或者直接使用pytest:
pytest tests/test_shopping_flow.py -v --html=reports/report.html --self-contained-html
项目扩展建议
这个实战项目可以进一步扩展:
-
数据驱动测试:使用CSV或JSON文件管理测试数据
-
API集成:结合API测试验证前后端一致性
-
性能测试:添加性能监控和断言
-
跨浏览器测试:扩展支持更多浏览器
-
移动端测试:添加移动端浏览器测试
-
可视化测试:集成视觉回归测试
-
CI/CD集成:配置GitHub Actions或Jenkins流水线
-
测试报告优化:集成更丰富的报告系统
这个实战项目涵盖了Selenium自动化测试的核心概念和最佳实践,包括页面对象模式、等待策略、异常处理、报告生成等。你可以根据实际需求进一步扩展和优化这个项目。