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 datetime
class Config:
# 浏览器配置
BROWSER = "chrome" # chrome, firefox, edge
HEADLESS = True
WINDOW_SIZE = "1920,1080"
IMPLICIT_WAIT = 10
# 测试网站URL
BASE_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"
@staticmethod
def get_timestamp():
return datetime.now().strftime("%Y%m%d_%H%M%S")
@staticmethod
def 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 allure
logger = logging.getLogger(__name__)
class BasePage:
def __init__(self, driver):
self.driver = driver
self.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}")
raise
def 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}")
raise
def 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}")
raise
def get_text(self, locator):
"""获取元素文本"""
element = self.find_element(locator)
return element.text
def is_element_present(self, locator):
"""检查元素是否存在"""
try:
self.find_element(locator)
return True
except (TimeoutException, NoSuchElementException):
return False
def 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_path
def 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 BasePage
class 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 True
return False
def go_to_cart(self):
"""前往购物车"""
self.click(self.SHOPPING_CART)
from .cart_page import CartPage
return CartPage(self.driver)
def sort_products(self, sort_by):
"""排序商品"""
from selenium.webdriver.support.ui import Select
dropdown = 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 Config
class 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 HomePage
return HomePage(self.driver)
def get_error_message(self):
"""获取错误消息"""
if self.is_element_present(self.ERROR_MESSAGE):
return self.get_text(self.ERROR_MESSAGE)
return None
def 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 BasePage
class 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 HomePage
return HomePage(self.driver)
创建 pages/cart_page.py
:
from selenium.webdriver.common.by import By
from .base_page import BasePage
class 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 True
return False
def continue_shopping(self):
"""继续购物"""
self.click(self.CONTINUE_SHOPPING_BUTTON)
from .home_page import HomePage
return HomePage(self.driver)
def checkout(self):
"""结算"""
self.click(self.CHECKOUT_BUTTON)
from .checkout_page import CheckoutPage
return CheckoutPage(self.driver)
创建 pages/checkout_page.py
:
from selenium.webdriver.common.by import By
from .base_page import BasePage
from utils.config import Config
class 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_NAME
last_name = last_name or Config.TEST_LAST_NAME
zip_code = zip_code or Config.TEST_ZIP_CODE
self.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 CartPage
return CartPage(self.driver)
def get_error_message(self):
"""获取错误消息"""
if self.is_element_present(self.ERROR_MESSAGE):
return self.get_text(self.ERROR_MESSAGE)
return None
def 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 HomePage
return 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 logging
logger = logging.getLogger(__name__)
def create_driver(browser_name=None, headless=None):
"""创建WebDriver实例"""
browser_name = browser_name or Config.BROWSER
headless = headless if headless is not None else Config.HEADLESS
if 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 driver
def 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()
yield
self.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 Config
def 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自动化测试的核心概念和最佳实践,包括页面对象模式、等待策略、异常处理、报告生成等。你可以根据实际需求进一步扩展和优化这个项目。