1. 项目概述:为什么我们需要一个“简单”的测试框架?

最近在带团队做项目,测试同学天天跟我抱怨,说回归测试的工作量太大了,每次发版前都要手动点点点,枯燥不说,还容易漏测。我一看,这不行啊,效率太低,而且人肉测试的稳定性也堪忧。于是,我决定带着他们搞一个自动化测试框架。目标很明确: 简单、好用、能快速落地 。我们不追求大而全的“银弹”,而是要一个团队成员(包括刚入行的测试新人)都能快速上手,能切实解决当前重复性功能验证问题的工具。

这就是“简单实现一个Python+Selenium的自动化测试框架”这个项目的由来。它本质上是一个 脚手架 ,帮你把自动化测试中那些繁琐、重复的准备工作(比如浏览器驱动管理、元素定位封装、测试报告生成、用例组织)都打包好,让你能专注于编写具体的测试逻辑。用Python+Selenium是因为这对测试同学来说学习曲线最平缓,生态也最成熟。这个框架能做什么?简单说,就是把你从“打开浏览器->输入网址->点击按钮->断言结果”这一系列手动操作中解放出来,用代码自动执行,并且生成清晰的测试报告,告诉你哪些用例过了,哪些挂了,为什么挂。

适合谁来参考?如果你是测试工程师,正苦于手工回归;如果你是开发,想为自己的模块补充一些自动化冒烟测试;甚至如果你是项目负责人,想提升团队的交付质量和效率,这个内容都值得一看。我们不会讲太深奥的设计模式,就从最实用的角度出发,一步步搭出一个能立刻用起来的框架。

2. 框架整体设计与核心思路拆解

在动手写代码之前,得先想清楚我们要做个什么东西。一个自动化测试框架,核心任务是把测试用例执行起来,并给出明确的结果。基于这个目标,我把它拆解成几个关键模块,这也是我们框架的骨架。

2.1 核心模块与职责划分

一个哪怕再简单的框架,也需要清晰的层次。我设计的结构主要分为四层,从上到下,职责逐渐细化:

  1. 测试用例层 :这是用户(测试工程师)主要工作的地方。他们在这里用自然语言(结合代码)描述测试场景,比如“用户登录成功”、“添加商品到购物车”。这一层应该尽量简洁,只关心业务逻辑和测试数据。
  2. 测试逻辑层 :这一层提供“积木”。我们把常用的页面操作(点击、输入、获取文本)和业务流(登录、退出)封装成一个个函数或类方法。用例层直接调用这些“积木”来搭建测试场景。这样做的好处是,当页面元素发生变化时,通常只需要修改这一层的某个“积木”,而不是去改每一个用例。
  3. 驱动与工具层 :这是框架的“发动机”和“工具箱”。负责管理WebDriver(控制浏览器的核心)、读取配置文件(如测试URL、浏览器类型)、生成测试报告、处理日志等。这些是支撑性的功能,为上层提供稳定的服务。
  4. 基础配置层 :存放所有环境相关的配置,比如 config.ini 文件、测试数据文件(如JSON、Excel)、日志存放路径等。实现配置与代码分离,不同环境(测试、预生产)切换起来非常方便。

这个分层结构的思想是“高内聚、低耦合”。用例不管浏览器怎么启动的,逻辑层不管报告怎么生成的,各司其职,维护和扩展起来就轻松很多。

2.2 技术选型背后的考量:为什么是Python + Selenium + Pytest?

市面上工具很多,我选择这个组合是基于以下几点实实在在的考虑:

  • Python :语法简洁,接近自然语言,对测试人员极其友好。庞大的第三方库( pytest , requests , openpyxl )让解决任何问题(数据驱动、报告生成)都像“拼积木”一样简单。团队学习成本最低。
  • Selenium :Web自动化测试的“事实标准”。支持所有主流浏览器,API成熟稳定,社区资源丰富。遇到问题,几乎总能搜到解决方案。对于Web UI自动化来说,它是目前最稳妥的选择。
  • Pytest :这是我框架的“测试执行引擎”。它比Python自带的 unittest 更强大、更灵活。 pytest 的夹具( fixture )机制,能优雅地解决测试前置(如初始化浏览器)和后置(如关闭浏览器、截图)操作。它的断言写法更直观(直接用 assert ),测试发现规则更智能,插件生态(如生成 allure 报告)更是杀手锏。

注意 :不要试图自己从头造轮子去管理用例执行、前置后置条件和报告。 Pytest 已经把这些做得非常好了,我们的框架应该是基于 Pytest 进行扩展和封装,而不是替代它。

2.3 目录结构规划:好的开始是成功的一半

代码还没写,我们先定好目录结构。清晰的目录能让项目一目了然,方便团队协作。我建议的目录结构如下:

automation_framework/
├── configs/               # 配置层
│   ├── config.ini         # 主配置文件
│   └── test_data.json     # 测试数据文件
├── logs/                  # 日志目录(运行时生成)
├── reports/               # 测试报告目录(运行时生成)
├── screenshots/           # 失败用例截图目录(运行时生成)
├── drivers/               # 浏览器驱动目录
│   ├── chromedriver(.exe)
│   └── geckodriver(.exe)
├── page_objects/          # 逻辑层:页面对象
│   ├── __init__.py
│   ├── base_page.py       # 基类,封装通用操作
│   ├── login_page.py      # 登录页面
│   └── main_page.py       # 主页
├── test_cases/            # 用例层
│   ├── __init__.py
│   ├── conftest.py        # pytest共享夹具配置
│   ├── test_login.py      # 登录相关测试用例
│   └── test_cart.py       # 购物车相关测试用例
├── utilities/             # 工具层
│   ├── __init__.py
│   ├── logger.py          # 日志工具
│   ├── config_reader.py   # 配置读取工具
│   └── report_generator.py # 报告生成工具(可整合allure)
└── requirements.txt       # 项目依赖包列表

这个结构不是一成不变的,你可以根据项目复杂度调整。但核心思想是: 配置、用例、页面对象、工具、输出物 ,各归其位。

3. 核心细节解析与实操要点

框架的骨架搭好了,现在我们开始填充血肉。这一部分,我们会深入几个最核心的模块,看看它们具体如何实现,以及实现过程中有哪些“坑”需要避开。

3.1 灵魂所在:BasePage基类的设计与封装

BasePage 是整个页面对象模型(Page Object Model, POM)的基石。它的核心思想是,将页面元素定位和常用操作封装起来,让具体的页面类(如 LoginPage )继承它,从而获得所有能力,并只需关注自己特有的元素和操作。

为什么要用POM? 想象一下,如果100个测试用例里都用 driver.find_element(By.ID, “username”).send_keys(“admin”) 来输入用户名。一旦这个输入框的ID变了,你就需要修改这100个地方。而POM模式下,你只需要在 LoginPage 类里修改一次 username_input 这个属性的定位方式。维护成本天差地别。

下面是一个实战中提炼出的 BasePage 类示例,包含了我认为最必要的封装:

# page_objects/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
import utilities.logger as log
import os

class BasePage:
    """所有页面对象的基类,封装通用Web操作和等待机制"""
    
    def __init__(self, driver):
        self.driver = driver
        self.log = log.get_logger() # 获取日志记录器
        self.wait = WebDriverWait(self.driver, 10) # 显式等待,超时10秒

    def open(self, url):
        """打开指定URL"""
        self.log.info(f"打开网址: {url}")
        self.driver.get(url)
        
    def find_element(self, locator):
        """查找单个元素,加入显式等待和日志"""
        try:
            self.log.debug(f"正在查找元素: {locator}")
            element = self.wait.until(EC.presence_of_element_located(locator))
            return element
        except TimeoutException:
            self.log.error(f"查找元素超时: {locator}")
            raise
            
    def click(self, locator):
        """点击元素"""
        element = self.find_element(locator)
        self.log.info(f"点击元素: {locator}")
        element.click()
        
    def input_text(self, locator, text):
        """向元素输入文本,并清空原有内容"""
        element = self.find_element(locator)
        element.clear()
        self.log.info(f"向元素 {locator} 输入文本: {text}")
        element.send_keys(text)
        
    def get_text(self, locator):
        """获取元素的文本内容"""
        element = self.find_element(locator)
        text = element.text
        self.log.debug(f"获取元素 {locator} 的文本: {text}")
        return text
        
    def is_element_visible(self, locator, timeout=5):
        """判断元素是否可见(用于断言)"""
        try:
            WebDriverWait(self.driver, timeout).until(
                EC.visibility_of_element_located(locator)
            )
            self.log.debug(f"元素可见: {locator}")
            return True
        except TimeoutException:
            self.log.debug(f"元素不可见: {locator}")
            return False
            
    def take_screenshot(self, name):
        """截取屏幕截图,保存到指定目录"""
        screenshot_dir = os.path.join(os.getcwd(), "screenshots")
        os.makedirs(screenshot_dir, exist_ok=True) # 目录不存在则创建
        file_path = os.path.join(screenshot_dir, f"{name}.png")
        self.driver.save_screenshot(file_path)
        self.log.info(f"截图已保存至: {file_path}")
        return file_path

封装要点与避坑指南:

  1. 统一的等待策略 Selenium 操作元素,90%的失败源于“元素未加载完成就进行操作”。我在 find_element 里强制集成了显式等待( WebDriverWait ),这是最重要的稳定性保障。默认10秒,可根据网络情况调整。
  2. 全面的日志记录 :每个关键操作(打开页面、点击、输入)都通过 log.info 记录,错误用 log.error 。调试时,通过日志文件就能清晰还原测试步骤,快速定位问题所在。
  3. 操作原子化 click input_text 等方法内部都调用了 find_element 。这样,具体的页面类(如 LoginPage )只需要关心“定位符”( locator )是什么,而不用重复写查找和等待的代码。
  4. 截图功能集成 :把截图方法放在基类里非常方便。在用例失败时,可以在 pytest 的夹具中自动调用这个方法,为问题排查留下直观证据。

3.2 驱动管理:告别手动下载和PATH配置

让每个团队成员手动下载浏览器驱动、配置系统PATH,是协作的噩梦。我们的框架必须解决这个问题。

方案:自动下载与匹配驱动 我们可以使用第三方库 webdriver-manager 。它会自动检测你电脑上的浏览器版本,并下载匹配的驱动到缓存目录,无需任何手动干预。

# 在conftest.py或专门的driver_manager.py中
from selenium import webdriver
from selenium.webdriver.chrome.service import Service as ChromeService
from webdriver_manager.chrome import ChromeDriverManager
from webdriver_manager.firefox import GeckoDriverManager

def get_driver(browser_name="chrome"):
    """根据配置创建并返回WebDriver实例"""
    driver = None
    if browser_name.lower() == "chrome":
        # 使用webdriver-manager自动管理驱动
        service = ChromeService(ChromeDriverManager().install())
        options = webdriver.ChromeOptions()
        # 常用选项:无头模式、忽略证书错误、禁用自动化提示
        # options.add_argument('--headless') # 无头模式,不打开浏览器窗口
        options.add_argument('--ignore-certificate-errors')
        options.add_experimental_option("excludeSwitches", ["enable-automation"])
        options.add_experimental_option('useAutomationExtension', False)
        driver = webdriver.Chrome(service=service, options=options)
        
    elif browser_name.lower() == "firefox":
        service = webdriver.FirefoxService(GeckoDriverManager().install())
        options = webdriver.FirefoxOptions()
        driver = webdriver.Firefox(service=service, options=options)
        
    else:
        raise ValueError(f"不支持的浏览器: {browser_name}")
    
    # 全局隐性等待(辅助,主要靠显式等待)
    driver.implicitly_wait(5)
    # 最大化窗口
    driver.maximize_window()
    return driver

实操心得 :虽然 webdriver-manager 很方便,但在公司内网环境可能无法连接外网下载。这时,备选方案是将常用版本的驱动(如 chromedriver )放在项目 drivers/ 目录下,然后在代码中指定驱动路径: service = ChromeService(executable_path=‘./drivers/chromedriver’) 。可以在配置文件中增加一个 driver_source 选项,让框架根据配置决定使用哪种方式。

3.3 配置文件管理:让框架灵活适应不同环境

测试环境( test )、预发布环境( staging )的URL、数据库连接、账号密码都不同。硬编码在代码里是绝对不可取的。我们用 configparser 库来读取 ini 格式的配置文件。

configs/config.ini 示例:

[DEFAULT]
log_level = INFO
browser = chrome
headless = false

[test]
base_url = https://test.example.com
username = test_user
password = test_pass123

[staging]
base_url = https://staging.example.com
username = staging_user
password = staging_pass456

配置读取工具 utilities/config_reader.py

import os
import configparser

class ConfigReader:
    """配置文件读取器,单例模式确保全局唯一配置"""
    
    _instance = None
    
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super(ConfigReader, cls).__new__(cls)
            cls._instance.config = configparser.ConfigParser()
            config_path = os.path.join(os.path.dirname(__file__), '..', 'configs', 'config.ini')
            cls._instance.config.read(config_path, encoding='utf-8')
        return cls._instance
    
    def get(self, section, option, fallback=None):
        """获取指定section和option的值"""
        return self.config.get(section, option, fallback=fallback)
    
    def getboolean(self, section, option, fallback=False):
        """获取布尔值"""
        return self.config.getboolean(section, option, fallback=fallback)

# 使用示例
config = ConfigReader()
base_url = config.get('test', 'base_url')
use_headless = config.getboolean('DEFAULT', 'headless')

这样,当我们需要切换测试环境时,只需要在命令行执行用例时传入一个参数(或修改配置文件的 [DEFAULT] 节),框架就会自动使用对应环境的配置,实现了代码与环境的解耦。

4. 实操过程:从零搭建框架核心环节

理论讲得差不多了,现在我们动手,把几个最关键的部分串联起来,形成一个可运行的闭环。我会以用户登录这个最经典的场景为例,展示完整的实现流程。

4.1 第一步:环境搭建与依赖安装

任何项目开始前,先准备好环境。我强烈建议使用虚拟环境( venv )来隔离项目依赖。

# 1. 创建项目目录并进入
mkdir automation_framework && cd automation_framework

# 2. 创建虚拟环境(Python3.3+内置)
python -m venv venv

# 3. 激活虚拟环境
# Windows:
venv\Scripts\activate
# Linux/Mac:
source venv/bin/activate

# 4. 创建requirements.txt文件,并写入核心依赖
# requirements.txt 内容:
# selenium>=4.0.0
# pytest>=7.0.0
# pytest-html>=3.0.0
# allure-pytest>=2.9.0
# webdriver-manager>=3.8.0
# openpyxl>=3.0.0  # 如果需要处理Excel测试数据

# 5. 安装依赖
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple

4.2 第二步:实现页面对象(以登录页面为例)

基于我们写好的 BasePage ,创建具体的页面类。这里以登录页面为例。

首先,定义定位符。我习惯用一个单独的类来管理,清晰且易于修改。

# page_objects/locators/login_page_locators.py
class LoginPageLocators:
    """登录页面所有元素的定位符"""
    # 使用 (By.策略, ‘值’) 的元组形式
    USERNAME_INPUT = (‘id’, ‘username’)  # 假设页面用户名输入框id为‘username’
    PASSWORD_INPUT = (‘id’, ‘password’)
    LOGIN_BUTTON = (‘xpath’, ‘//button[@type=“submit”]’)
    ERROR_MESSAGE = (‘css selector’, ‘.alert.alert-danger’)
    SUCCESS_INDICATOR = (‘xpath’, ‘//h1[contains(text(), “Dashboard”)]’)

然后,实现登录页面类。

# page_objects/login_page.py
from selenium.webdriver.common.by import By
from .base_page import BasePage
from .locators.login_page_locators import LoginPageLocators

class LoginPage(BasePage):
    """登录页面操作封装"""
    
    def __init__(self, driver):
        super().__init__(driver) # 调用父类初始化,传入driver
    
    def enter_username(self, username):
        """输入用户名"""
        self.input_text(LoginPageLocators.USERNAME_INPUT, username)
        
    def enter_password(self, password):
        """输入密码"""
        self.input_text(LoginPageLocators.PASSWORD_INPUT, password)
        
    def click_login(self):
        """点击登录按钮"""
        self.click(LoginPageLocators.LOGIN_BUTTON)
        
    def login(self, username, password):
        """登录完整流程:输入用户名、密码、点击登录"""
        self.log.info(f”执行登录操作,用户名: {username}“)
        self.enter_username(username)
        self.enter_password(password)
        self.click_login()
        
    def get_error_message(self):
        """获取登录错误提示信息,如果存在则返回文本,否则返回空字符串"""
        if self.is_element_visible(LoginPageLocators.ERROR_MESSAGE, timeout=3):
            return self.get_text(LoginPageLocators.ERROR_MESSAGE)
        return ”“
        
    def is_login_successful(self):
        """通过检查登录后页面特定元素(如Dashboard标题)来判断是否登录成功"""
        return self.is_element_visible(LoginPageLocators.SUCCESS_INDICATOR, timeout=10)

可以看到, LoginPage 类非常干净,它只包含这个页面特有的操作。所有与浏览器交互的底层细节(等待、查找、点击)都被 BasePage 消化了。这就是封装的魅力。

4.3 第三步:编写Pytest测试用例与夹具

现在,我们用 pytest 来编写和组织测试用例。 conftest.py pytest 的本地插件文件,在这里我们可以定义全局共享的夹具( fixture )。

# test_cases/conftest.py
import pytest
from utilities.driver_manager import get_driver # 假设我们把get_driver函数放在这里
from utilities.config_reader import ConfigReader

config = ConfigReader()

@pytest.fixture(scope=“function”)
def driver():
    """为每个测试函数提供独立的driver实例"""
    browser = config.get(‘DEFAULT’, ‘browser’, fallback=‘chrome’)
    headless = config.getboolean(‘DEFAULT’, ‘headless’, fallback=False)
    
    # 这里可以基于headless参数调整浏览器选项,代码略
    driver_instance = get_driver(browser)
    yield driver_instance # yield之前是setup,之后是teardown
    # 测试函数执行完毕后,执行清理
    driver_instance.quit()
    print(”浏览器已关闭“)

@pytest.fixture(scope=“function”)
def login_page(driver):
    """提供一个初始化好的LoginPage实例"""
    from page_objects.login_page import LoginPage
    base_url = config.get(‘test’, ‘base_url’)
    page = LoginPage(driver)
    page.open(base_url + ‘/login’) # 拼接登录页具体路径
    return page

夹具 driver 的作用域是 function ,意味着每个测试用例都会启动和关闭一次浏览器,保证用例间的隔离。 login_page 夹具则直接返回一个已经打开了登录页面的 LoginPage 对象,方便用例使用。

接下来,编写真正的测试用例。

# test_cases/test_login.py
import pytest
from utilities.config_reader import ConfigReader

config = ConfigReader()

class TestLogin:
    """登录功能测试集"""
    
    def test_login_success(self, login_page):
        """测试正常登录流程"""
        username = config.get(‘test’, ‘username’)
        password = config.get(‘test’, ‘password’)
        
        login_page.login(username, password)
        
        # 断言:登录成功后,应该能看到成功标识(如Dashboard标题)
        assert login_page.is_login_successful(), f”登录失败,未找到成功页面元素“
        # 可以添加更多断言,如检查URL、页面标题等
        # assert “dashboard” in login_page.driver.current_url.lower()
        
    def test_login_failure_with_wrong_password(self, login_page):
        """测试使用错误密码登录"""
        username = config.get(‘test’, ‘username’)
        wrong_password = “wrong_password”
        
        login_page.login(username, wrong_password)
        
        # 断言:应该出现错误提示信息
        error_msg = login_page.get_error_message()
        assert error_msg != ”“, “预期出现错误提示,但未找到”
        assert “密码错误” in error_msg or “invalid” in error_msg.lower() # 根据实际提示调整
        
    @pytest.mark.parametrize(“username, password, expected”, [
        (“”, “test_pass”, “用户名不能为空”), # 用户名空
        (“test_user”, “”, “密码不能为空”), # 密码空
        (“not_exist”, “test_pass”, “用户不存在”), # 用户不存在
    ])
    def test_login_validation(self, login_page, username, password, expected):
        """参数化测试:测试各种边界和异常情况"""
        login_page.login(username, password)
        error_msg = login_page.get_error_message()
        assert expected in error_msg, f”错误提示不符。预期包含‘{expected}’,实际为‘{error_msg}’“

这个测试文件展示了三种典型的测试用例写法:1)正常流程测试;2)异常流程测试;3)使用 @pytest.mark.parametrize 的参数化测试,用一组数据驱动多个测试场景,极大减少了代码重复。

4.4 第四步:生成漂亮的测试报告

测试跑完了,结果不能只停留在控制台。我们需要一份直观、详细的报告。 pytest-html 可以生成基础的HTML报告,而 allure 则可以生成非常美观且功能强大的报告。

使用pytest-html(简单快捷):

# 运行测试并生成html报告
pytest test_cases/test_login.py -v --html=reports/report.html --self-contained-html

--self-contained-html 参数会把CSS等资源打包进一个HTML文件,方便分享。

使用Allure(推荐,更专业):

  1. 首先需要安装Allure命令行工具(从官网下载或通过包管理器)。
  2. 运行测试时,让 pytest 生成 allure 结果数据。
    pytest test_cases/test_login.py -v --alluredir=./reports/allure-results
    
  3. 生成并打开 Allure 报告。
    allure generate ./reports/allure-results -o ./reports/allure-report --clean
    allure open ./reports/allure-report
    

Allure 报告会展示用例执行时间、通过率、失败截图、步骤详情等,非常适合在团队中分享和进行问题分析。我们可以把生成报告的步骤写进一个 run_tests.py 脚本或 Makefile 里,一键执行。

5. 常见问题与排查技巧实录

框架搭起来了,用例也写了,但在实际运行中,你一定会遇到各种各样的问题。下面是我在多年实践中总结的一些高频问题和解决思路,相当于一份“急救手册”。

5.1 元素定位失败:自动化测试的头号杀手

超过70%的自动化测试失败源于元素定位问题。错误信息通常是 NoSuchElementException TimeoutException

排查清单:

  1. 检查定位符是否正确 :这是第一步也是最常见的一步。用浏览器的开发者工具(F12)重新检查元素的 id name class XPath 是否唯一、是否稳定。警惕动态ID(包含随机字符串)。
  2. 检查页面是否加载完成 :虽然我们用了显式等待,但等待条件可能不对。 presence_of_element_located 只要求元素存在于DOM中,但可能不可见或不可交互。如果是要点击或输入,尝试改用 element_to_be_clickable visibility_of_element_located
    # 更稳健的点击操作封装
    def click_safely(self, locator):
        element = self.wait.until(EC.element_to_be_clickable(locator))
        element.click()
    
  3. 检查是否有iframe :如果目标元素在 iframe 内部,你必须先切换到对应的 iframe ,才能定位到里面的元素。
    # 切换到iframe
    iframe = self.find_element((‘tag name‘, ’iframe‘))
    self.driver.switch_to.frame(iframe)
    # 操作iframe内的元素...
    # 操作完成后切回主文档
    self.driver.switch_to.default_content()
    
  4. 检查是否有新窗口/标签页 :点击某个链接后打开了新窗口,driver需要切换过去。
    # 获取当前所有窗口句柄
    original_window = self.driver.current_window_handle
    # 点击打开新窗口的操作...
    # 等待新窗口出现
    WebDriverWait(self.driver, 10).until(EC.number_of_windows_to_be(2))
    # 循环找到新窗口句柄并切换
    for window_handle in self.driver.window_handles:
        if window_handle != original_window:
            self.driver.switch_to.window(window_handle)
            break
    
  5. 等待时间不足 :网络慢或页面渲染慢。适当增加显式等待的超时时间,或者在操作前加入固定的 sleep (不推荐,应作为最后手段)或等待特定条件(如某个加载动画消失)。

实操心得 :对于难以定位或不稳定的元素,不要死磕 XPath CSS 。可以尝试与开发沟通,给关键测试元素添加固定的 id data-testid 属性。这是提升自动化稳定性的最有效合作方式。

5.2 测试用例的独立性维护

测试用例之间应该是独立的,不依赖执行顺序。但有时会因为缓存、登录状态、浏览器数据残留导致用例间相互影响。

解决方案:

  • 使用 pytest function 作用域夹具 :正如我们之前做的,每个用例都重新启动和关闭浏览器,这是最彻底的隔离,但代价是执行时间变长。
  • 在用例开始前清理状态 :对于不能每个用例都重启浏览器的场景(如性能测试),可以在用例的 setup 阶段(或使用 @pytest.fixture(autouse=True) )执行清理操作,如清除 cookies localStorage
    @pytest.fixture(autouse=True)
    def clear_browser_data(self, driver):
        """每个测试自动执行:清理cookies和local storage"""
        driver.delete_all_cookies()
        driver.execute_script(“window.localStorage.clear();”)
        yield
    
  • 使用不同的测试账号 :对于需要登录的测试,准备一套独立的测试账号池,避免多个用例同时操作同一个账号的数据导致冲突。

5.3 测试数据的管理与参数化

测试数据(如用户名、商品ID)硬编码在用例里是坏习惯。管理测试数据,我推荐两种方式:

  1. JSON/YAML文件 :适合结构简单、层次清晰的数据。
    // test_data/login_data.json
    {
      “valid_credentials”: {
        “username”: “standard_user”,
        “password”: “secret_sauce”
      },
      “invalid_credentials”: [
        {“username”: “”, “password”: “secret_sauce”, “error”: “Username is required”},
        {“username”: “locked_user”, “password”: “secret_sauce”, “error”: “Sorry, this user has been locked out.”}
      ]
    }
    
  2. Excel/CSV文件 :适合测试人员维护,尤其是大量、表格化的数据。可以使用 openpyxl pandas 库读取。
  3. @pytest.mark.parametrize 装饰器 :对于直接在代码里定义的小规模参数化数据,这是最方便的方式,如前文示例。

最佳实践是将静态的、大量的测试数据放在外部文件(JSON/Excel)中,在 conftest.py 里读取并转换为 pytest 可用的参数化格式,或者通过自定义夹具提供给用例。

5.4 如何在CI/CD流水线中运行(无头模式)

在Jenkins、GitLab CI等持续集成环境中,通常没有图形界面。这时需要以“无头模式”运行浏览器。

修改 get_driver 函数,增加无头模式选项:

def get_driver(browser_name=“chrome”, headless=False):
    if browser_name.lower() == “chrome”:
        options = webdriver.ChromeOptions()
        if headless:
            options.add_argument(‘--headless=new’) # Chrome较新版本推荐使用new
        options.add_argument(‘--no-sandbox’) # Linux CI环境常需此参数
        options.add_argument(‘--disable-dev-shm-usage’) # 解决共享内存问题
        options.add_argument(‘--disable-gpu’) # 可选
        options.add_argument(‘--window-size=1920,1080’) # 设置无头模式下的窗口大小
        # ... 其他选项
        driver = webdriver.Chrome(service=service, options=options)
    # ... firefox 类似

在CI的配置文件中,通过环境变量或配置文件将 headless 设置为 True 即可。

5.5 失败用例自动截图与日志追踪

测试失败时,光有错误堆栈是不够的。自动截图和详尽的日志是快速定位问题的关键。

我们可以在 pytest 的夹具中,捕获测试失败的事件,并自动调用我们 BasePage 里的截图方法。

# test_cases/conftest.py
import pytest
from utilities.config_reader import ConfigReader

config = ConfigReader()

@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
    """Hook函数,用于在测试执行后获取结果并截图"""
    outcome = yield
    report = outcome.get_result()
    
    # 只关注测试用例(不包括setup/teardown)的失败情况
    if report.when == “call” and report.failed:
        # 从测试用例中获取driver对象(假设driver夹具名为‘driver’)
        driver_fixture = item.funcargs.get(‘driver’)
        if driver_fixture:
            # 生成唯一的截图文件名
            screenshot_name = f”{item.name}_{report.when}”
            try:
                # 调用driver的截图方法,这里需要你的page对象或driver有截图方法
                # 假设我们有一个全局可用的截图工具函数
                from utilities.screenshot_helper import take_screenshot
                take_screenshot(driver_fixture, screenshot_name)
                # 或者,如果测试用例使用了page对象,可以这样:
                # page = item.funcargs.get(‘login_page’)
                # if page:
                #     page.take_screenshot(screenshot_name)
                print(f”失败截图已保存: {screenshot_name}“)
            except Exception as e:
                print(f”截图失败: {e}“)

同时,确保我们的日志配置( utilities/logger.py )将不同级别的日志输出到文件,这样结合 Allure 报告,就能形成一个从“测试步骤日志”到“最终错误截图”的完整问题追溯链。

搭建一个“简单”的自动化测试框架,远不止是把几行 Selenium 代码包装起来。它涉及到项目结构设计、代码封装、配置管理、报告生成和持续集成等一系列工程化实践。这个过程中,最大的挑战往往不是技术本身,而是如何让框架足够健壮以应对千变万化的Web页面,以及如何让它足够简单以被团队所有成员接受和使用。我分享的这个框架雏形,已经包含了应对这些挑战的核心思路和关键实现。你可以以此为基础,根据自己项目的实际需求,添加更多功能,比如数据库校验、API调用混合测试、分布式测试支持等。记住,框架是为人服务的,切忌过度设计,解决实际问题的框架,才是好框架。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐