Selenium 实战项目:电子商务网站自动化测试

发布于:2025-09-01 ⋅ 阅读:(16) ⋅ 点赞:(0)

        Selenium 实战项目 | 菜鸟 ,建议你先看完前面这篇再来看下面这篇。

下面我将带你完成一个完整的 Selenium 实战项目,模拟用户在一个电子商务网站上的完整购物流程。这个项目将综合运用我们之前学到的所有知识。

项目概述

我们将创建一个自动化测试脚本,模拟用户在电子商务网站上完成以下操作:

  1. 访问网站首页

  2. 用户登录

  3. 浏览商品分类

  4. 搜索商品

  5. 将商品添加到购物车

  6. 查看购物车

  7. 进入结算流程

  8. 填写配送信息

  9. 选择支付方式

  10. 完成订单

项目结构

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

项目扩展建议

这个实战项目可以进一步扩展:

  1. 数据驱动测试:使用CSV或JSON文件管理测试数据

  2. API集成:结合API测试验证前后端一致性

  3. 性能测试:添加性能监控和断言

  4. 跨浏览器测试:扩展支持更多浏览器

  5. 移动端测试:添加移动端浏览器测试

  6. 可视化测试:集成视觉回归测试

  7. CI/CD集成:配置GitHub Actions或Jenkins流水线

  8. 测试报告优化:集成更丰富的报告系统

这个实战项目涵盖了Selenium自动化测试的核心概念和最佳实践,包括页面对象模式、等待策略、异常处理、报告生成等。你可以根据实际需求进一步扩展和优化这个项目。