从零搭建Python Selenium自动化测试框架:POM设计与Pytest实践

1. 项目概述:为什么我们需要一个自己的自动化测试框架?

如果你是一名测试工程师,或者正在学习Python自动化测试,那么“Selenium”这个名字你一定不陌生。它就像一把万能钥匙,能让你用代码控制浏览器,模拟用户点击、输入、滚动等一系列操作。但很多朋友在入门后会发现,仅仅会写几个find_elementclick是远远不够的。当测试用例从几个变成几十个、几百个时,脚本会变得混乱不堪:重复的登录代码到处都是,元素定位器散落在各个角落,一个页面改了,所有脚本都得跟着改,维护成本高得吓人。

这就是为什么我们需要一个“框架”。它不是一个遥不可及的概念,而是一套帮你把散乱的积木(单个测试脚本)搭建成稳固城堡(可维护、可扩展的测试体系)的规则和工具。今天,我们不谈那些庞大复杂的商业框架,就用最接地气的Python + Selenium + PyCharm,从零开始,手把手搭建一个属于你自己的、五脏俱全的自动化测试框架。这个框架将涵盖用例管理、页面对象模型、数据驱动、日志报告等核心模块,让你彻底告别“脚本小子”阶段,写出真正专业、高效的自动化测试代码。无论你是刚学完Selenium基础的新手,还是苦于脚本维护的老手,这篇文章都将为你提供一条清晰的路径。

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

在动手写代码之前,理清思路至关重要。一个好的框架不是功能的堆砌,而是为了解决特定问题而设计的结构。我们的核心目标是:提升脚本的可维护性、可读性和复用性。基于这个目标,我们采用业界公认的最佳实践来设计框架的骨架。

2.1 为什么选择“页面对象模型”作为基石?

页面对象模型(Page Object Model, POM)是Selenium自动化测试的黄金法则。它的核心思想是将测试逻辑页面元素定位与操作分离开。想象一下,如果没有POM,你的测试脚本会直接充斥着像driver.find_element(By.ID, “username”).send_keys(“admin”)这样的代码。一旦页面上用户名输入框的ID变了,你就得在所有用到它的脚本里一个个去修改,这无疑是场灾难。

POM的做法是,为每一个被测试的网页(或页面中的一个功能区域)创建一个对应的Python类。这个类不关心测试用例是什么,它只做两件事:

  1. 封装元素定位器:将所有这个页面的元素定位方式(如ID、XPath、CSS选择器)定义为类的属性。
  2. 封装页面操作:将在这个页面上可能进行的操作(如输入、点击、获取文本)定义为类的方法。

这样,测试用例脚本就变得非常清爽:login_page.input_username(“admin”)login_page.click_submit()。即使页面元素变了,你也只需要去修改对应的Page类中的一个地方,所有测试用例都会自动生效。这极大地降低了维护成本,也使得非技术人员(如产品经理)能更容易地理解测试用例在做什么。

2.2 目录结构规划:清晰即高效

一个清晰的目录结构是框架可维护性的物理体现。在PyCharm中新建一个项目,我们建议按如下方式组织:

your_automation_framework/ ├── common/ # 公共组件 │ ├── __init__.py │ ├── base_page.py # 所有Page类的基类 │ ├── webdriver_utils.py # 浏览器驱动工具类 │ └── logger.py # 日志记录模块 ├── page_objects/ # 页面对象类 │ ├── __init__.py │ ├── login_page.py │ ├── home_page.py │ └── search_page.py ├── test_cases/ # 测试用例 │ ├── __init__.py │ ├── test_login.py │ └── test_search.py ├── test_data/ # 测试数据 │ ├── __init__.py │ └── data.json 或 data.csv ├── reports/ # 测试报告(执行后生成) │ └── (存放HTML报告、截图等) ├── logs/ # 运行日志(执行后生成) │ └── (存放.log文件) ├── conftest.py # Pytest的共享Fixture配置(如果使用Pytest) └── run_tests.py # 测试执行入口文件

这个结构将不同职责的代码模块化,各司其职。common放通用工具,page_objects专注页面,test_cases专注业务逻辑,test_data管理输入,reportslogs存放输出。无论项目规模如何增长,你都能快速找到需要修改的代码。

2.3 工具选型:为什么是Pytest + Allure?

虽然Python自带的unittest模块也能用,但我们强烈推荐使用Pytest作为测试运行器。原因很简单:更简洁、更强大、更灵活Pytest的语法更符合Python风格(不需要继承某个类),夹具(Fixture)机制(特别是@pytest.fixture)能优雅地管理测试前置(如初始化浏览器)和后置(如关闭浏览器、截图)操作,参数化测试也异常方便。

对于测试报告,HTMLTestRunner是经典选择,但Allure报告在美观度和信息呈现上更胜一筹。它能生成非常专业的交互式报告,清晰地展示测试套件、用例层级、步骤详情、附件(截图、日志)等,是向团队展示测试结果的有力工具。我们将把Allure集成进来,作为框架的报告模块。

注意:工具选型没有绝对的对错,关键是适合团队和项目。这里的选择是基于当前Python测试生态的主流和最佳实践,能让你在学习和工作中与社区接轨。

3. 核心模块详解与实操要点

有了设计蓝图,我们就可以开始一砖一瓦地搭建了。我们从最基础的、也是最重要的几个模块开始。

3.1 环境准备与依赖安装

工欲善其事,必先利其器。确保你的电脑上已经安装了Python(建议3.8及以上版本)和PyCharm(社区版即可)。接下来,我们通过命令行(PyCharm的Terminal或系统CMD)安装必要的Python库。

# 核心测试库 pip install selenium # 测试运行与框架 pip install pytest pip install pytest-html # 生成简易HTML报告 pip install pytest-xdist # 可选,用于并行测试 # 高级报告系统 pip install allure-pytest # 数据驱动支持(如使用Excel) pip install openpyxl pandas

浏览器驱动管理:这是新手最容易踩坑的地方。Selenium需要通过一个“驱动程序”(如chromedriver)来与实际的浏览器(如Chrome)对话。你需要根据本地安装的Chrome浏览器版本,去下载对应版本的chromedriver。一个省事的做法是使用webdriver-manager库,它可以自动下载和管理匹配的驱动。

pip install webdriver-manager

在代码中,你可以这样使用它来避免手动下载和配置环境变量的麻烦:

from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))

3.2 打造坚如磐石的BasePage

BasePage是所有具体页面对象类(如LoginPage)的父类。它封装了所有页面都可能用到的最基本操作,实现了“一次编写,处处复用”。这是框架复用的核心。

# common/base_page.py import logging from datetime import datetime from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import TimeoutException, NoSuchElementException class BasePage: """所有页面类的基类,封装通用方法""" def __init__(self, driver): self.driver = driver self.logger = logging.getLogger(__name__) self.wait = WebDriverWait(driver, 10) # 显式等待,超时10秒 def find_element(self, locator): """查找单个元素,加入显式等待和日志""" try: self.logger.info(f"正在查找元素: {locator}") element = self.wait.until(EC.presence_of_element_located(locator)) return element except TimeoutException: self.logger.error(f"元素查找超时: {locator}") self._take_screenshot("element_not_found") raise def click(self, locator): """点击元素""" element = self.find_element(locator) self.logger.info(f"点击元素: {locator}") element.click() def input_text(self, locator, text): """向元素输入文本""" element = self.find_element(locator) self.logger.info(f"向元素 {locator} 输入文本: {text}") element.clear() element.send_keys(text) def get_text(self, locator): """获取元素文本""" element = self.find_element(locator) text = element.text self.logger.info(f"获取元素 {locator} 的文本: {text}") return text def _take_screenshot(self, name): """内部方法:截图并保存到报告目录""" timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") screenshot_path = f"./reports/screenshots/{name}_{timestamp}.png" self.driver.save_screenshot(screenshot_path) self.logger.info(f"截图已保存至: {screenshot_path}") return screenshot_path def is_element_visible(self, locator, timeout=5): """判断元素是否可见(快速检查)""" try: WebDriverWait(self.driver, timeout).until(EC.visibility_of_element_located(locator)) return True except TimeoutException: return False

关键点解析

  1. 显式等待WebDriverWait是处理页面元素加载异步问题的利器。比起time.sleep()这种“硬等待”,它更智能,会在条件满足(如元素出现、可点击)后立即继续,最多等待指定时间。这能显著提升测试执行速度。
  2. 集中日志:每个操作都通过logging模块记录,级别分为INFO(正常操作)、ERROR(失败异常)。当测试失败时,查看日志能快速定位问题发生在哪个步骤。
  3. 失败截图:在find_element失败时自动截图,这是调试的“后悔药”。截图文件名包含时间戳,避免覆盖,并统一存放到reports/screenshots目录,方便与报告关联。
  4. 通用操作封装click,input_text,get_text这些高频操作被标准化,子类页面对象直接调用即可,代码简洁且统一。

3.3 实现第一个页面对象:LoginPage

现在,我们用BasePage来构建一个具体的登录页面类。假设我们有一个简单的登录页,用户名输入框ID是username,密码框ID是password,登录按钮ID是submit

# page_objects/login_page.py from selenium.webdriver.common.by import By from common.base_page import BasePage class LoginPage(BasePage): """登录页面对象模型""" # 定位器:将页面元素定位方式集中管理 USERNAME_INPUT = (By.ID, "username") PASSWORD_INPUT = (By.ID, "password") SUBMIT_BUTTON = (By.ID, "submit") ERROR_MSG_SPAN = (By.CLASS_NAME, "error-message") def __init__(self, driver): super().__init__(driver) # 调用父类初始化方法 self.driver = driver # 可以在这里添加页面特有的初始化逻辑,比如访问登录页URL # self.driver.get("https://your-app.com/login") def login(self, username, password): """登录操作:输入用户名、密码并点击提交""" self.logger.info(f"执行登录操作,用户名: {username}") self.input_text(self.USERNAME_INPUT, username) self.input_text(self.PASSWORD_INPUT, password) self.click(self.SUBMIT_BUTTON) # 登录后通常页面会跳转,可以返回下一个页面的对象,如HomePage # from page_objects.home_page import HomePage # return HomePage(self.driver) def get_error_message(self): """获取登录错误提示信息""" if self.is_element_visible(self.ERROR_MSG_SPAN): return self.get_text(self.ERROR_MSG_SPAN) return None def is_login_page_loaded(self): """检查登录页面是否成功加载""" return self.is_element_visible(self.USERNAME_INPUT)

实操心得

  • 定位器管理:将所有定位器定义为类变量(By.ID, “username”),这是POM的核心。如果页面元素变了,你只需要修改这里的常量值,所有用到它的方法都会自动更新。
  • 操作组合login方法将输入用户名、密码、点击登录这三个步骤组合成一个业务操作。测试用例中只需调用page.login(“user”, “pass”),极大提升了可读性。
  • 页面跳转处理:登录成功后往往进入首页。好的实践是让login方法返回下一个页面的对象(如HomePage),这样测试用例可以链式调用:home_page = login_page.login(…); home_page.do_something(),流程非常清晰。

3.4 编写你的第一个Pytest测试用例

有了LoginPage,我们现在可以编写一个真正的测试用例了。使用Pytest,测试文件以test_开头,测试函数也以test_开头。

# test_cases/test_login.py import pytest import logging from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager from page_objects.login_page import LoginPage class TestLogin: """登录功能测试类""" @pytest.fixture(scope="class") def driver(self): """Fixture: 为整个测试类启动和关闭浏览器""" # 使用webdriver-manager自动管理驱动 service = Service(ChromeDriverManager().install()) # 配置浏览器选项,例如无头模式、忽略证书错误等 options = webdriver.ChromeOptions() options.add_argument('--ignore-certificate-errors') # options.add_argument('--headless') # 无头模式,不打开GUI窗口 driver = webdriver.Chrome(service=service, options=options) driver.maximize_window() # 最大化窗口 driver.implicitly_wait(5) # 隐式等待,作为查找元素的全局超时 yield driver # 将driver对象提供给测试用例使用 # 所有测试用例执行完毕后,执行清理工作 driver.quit() logging.info("浏览器已关闭。") @pytest.fixture def login_page(self, driver): """Fixture: 初始化登录页面对象,每个测试函数都会重新初始化""" # 这里假设登录页的URL,实际项目中可以从配置文件中读取 driver.get("https://your-app.com/login") page = LoginPage(driver) assert page.is_login_page_loaded(), "登录页面未能成功加载" return page def test_login_success(self, login_page): """测试用例:使用正确的用户名和密码登录成功""" # 准备测试数据 username = "correct_user" password = "correct_password" # 执行操作 login_page.login(username, password) # 验证断言:登录成功后,应该跳转到首页,首页会有特定的欢迎元素 # 这里以检查URL是否包含“dashboard”为例 assert "dashboard" in login_page.driver.current_url.lower() logging.info(f"登录成功,当前URL: {login_page.driver.current_url}") def test_login_failure_with_wrong_password(self, login_page): """测试用例:使用错误密码登录,应显示错误信息""" username = "correct_user" wrong_password = "wrong_password" login_page.login(username, wrong_password) # 验证断言:页面应显示错误提示信息 error_msg = login_page.get_error_message() assert error_msg is not None, "未找到错误提示信息" assert "密码错误" in error_msg or "invalid" in error_msg.lower() logging.info(f"登录失败,错误信息: {error_msg}") @pytest.mark.parametrize("username, password", [ ("", "somepassword"), # 用户名为空 ("someuser", ""), # 密码为空 ("", ""), # 都为空 ]) def test_login_failure_with_empty_credentials(self, login_page, username, password): """参数化测试:测试用户名或密码为空的登录失败场景""" login_page.login(username, password) error_msg = login_page.get_error_message() assert error_msg is not None # 根据实际应用提示信息调整断言内容 assert "不能为空" in error_msg or "required" in error_msg.lower()

关键点解析

  1. Pytest Fixture (@pytest.fixture):这是Pytest的灵魂。driver这个fixture的scope=”class”表示整个TestLogin类只启动一次浏览器,所有测试用例共用,最后统一关闭。login_page这个fixture的scope默认是function,表示每个测试函数都会重新获取一个新的页面对象,保证了测试之间的独立性。
  2. yield关键字:在fixture中,yield之前的代码是“设置”(setup),yield返回的是提供给测试用例的对象,yield之后的代码是“清理”(teardown)。这种写法非常清晰。
  3. 隐式等待与显式等待结合:在driverfixture中设置了implicitly_wait(5)作为全局兜底。在BasePage中我们主要使用更精确的显式等待。两者结合,既保证了效率,又增强了健壮性。
  4. 参数化测试 (@pytest.mark.parametrize):这是Pytest的一大亮点。一个测试函数,通过装饰器传入多组数据,就能自动生成多个测试用例。这极大地减少了重复代码,非常适合测试边界值和多种输入组合的场景。
  5. 断言(Assert):测试的核心是验证。使用Python标准的assert语句,如果条件为False,测试就会失败,并输出错误信息。断言应该清晰、具体地描述期望的结果。

4. 高级功能集成与框架完善

基础框架搭建完成后,我们可以引入更多提升效率和体验的组件。

4.1 数据驱动测试:让测试数据与代码分离

硬编码的测试数据(如username = “testuser”)不利于维护和扩展。数据驱动测试(Data-Driven Testing, DDT)将测试数据存储在外部文件(如JSON、CSV、Excel)中,测试用例从文件中读取数据执行。这样,增加测试场景只需修改数据文件,无需改动代码。

我们以JSON为例:

// test_data/login_data.json [ { "test_case": "login_success", "username": "standard_user", "password": "secret_sauce", "expected_result": "success", "expected_url_contains": "inventory" }, { "test_case": "login_locked_user", "username": "locked_out_user", "password": "secret_sauce", "expected_result": "failure", "expected_error_msg": "Epic sadface: Sorry, this user has been locked out." }, { "test_case": "login_wrong_password", "username": "standard_user", "password": "wrong", "expected_result": "failure", "expected_error_msg": "Epic sadface: Username and password do not match any user in this service" } ]

然后,在测试用例中读取并使用这些数据:

# test_cases/test_login_ddt.py import json import pytest class TestLoginDDT: @pytest.fixture def load_login_data(self): """Fixture: 从JSON文件加载测试数据""" with open('./test_data/login_data.json', 'r', encoding='utf-8') as f: data = json.load(f) return data @pytest.mark.parametrize("data_item", load_login_data.__wrapped__({})) def test_login_with_data(self, login_page, data_item): """使用数据驱动的登录测试""" username = data_item["username"] password = data_item["password"] expected_result = data_item["expected_result"] login_page.login(username, password) if expected_result == "success": assert data_item["expected_url_contains"] in login_page.driver.current_url elif expected_result == "failure": error_msg = login_page.get_error_message() assert error_msg is not None assert data_item["expected_error_msg"] in error_msg

注意:这里为了简化演示,直接在一个测试函数里处理了成功和失败两种断言逻辑。更复杂的场景可以为成功和失败分别编写测试函数,或者使用更灵活的数据结构。

4.2 生成Allure测试报告

Allure报告能提供无与伦比的测试执行洞察力。首先确保已安装allure-pytestAllure命令行工具(可从官网下载并配置系统PATH)。

pytest执行命令中加入Allure相关参数:

# 在项目根目录下执行 pytest test_cases/ -v --alluredir=./reports/allure_raw

这条命令会运行test_cases目录下的所有测试,并将原始的测试结果数据(一堆JSON文件)输出到./reports/allure_raw目录。

然后,使用Allure命令行工具生成可交互的HTML报告:

allure generate ./reports/allure_raw -o ./reports/allure_html --clean allure open ./reports/allure_html

allure generate命令将原始数据转换成漂亮的HTML报告,allure open会在默认浏览器中打开它。报告里可以看到测试套件树、通过率、耗时、每个测试用例的详细步骤(需要在代码中用@allure.step装饰器标记)、以及我们之前附加的截图和日志,信息一目了然。

为了让步骤更清晰,我们可以在页面对象的方法上添加Allure步骤装饰器:

# 修改 common/base_page.py 或 page_objects/login_page.py import allure class LoginPage(BasePage): @allure.step("登录操作:输入用户名 '{username}' 和密码") def login(self, username, password): self.input_text(self.USERNAME_INPUT, username) self.input_text(self.PASSWORD_INPUT, password) self.click(self.SUBMIT_BUTTON)

4.3 配置化与日志系统

一个成熟的框架需要良好的配置管理和日志记录。

配置文件:使用config.iniconfig.yaml来管理环境变量、URL、超时时间、浏览器类型等。

# config.ini [ENVIRONMENT] base_url = https://your-app.com browser = chrome headless = False [TIMEOUT] implicit_wait = 5 explicit_wait = 10 page_load_timeout = 30 [PATH] screenshot_dir = ./reports/screenshots log_dir = ./logs

在框架中用一个Config类来读取这些配置。

日志系统:使用Python的logging模块,配置不同的处理器(Handler),将日志同时输出到控制台和文件。

# common/logger.py import logging import os from datetime import datetime def setup_logger(name, log_level=logging.INFO): """配置并返回一个logger实例""" # 创建logger logger = logging.getLogger(name) logger.setLevel(log_level) # 避免重复添加handler if logger.handlers: return logger # 定义日志格式 formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') # 控制台处理器 console_handler = logging.StreamHandler() console_handler.setLevel(log_level) console_handler.setFormatter(formatter) logger.addHandler(console_handler) # 文件处理器 log_dir = "./logs" os.makedirs(log_dir, exist_ok=True) log_file = os.path.join(log_dir, f"automation_{datetime.now().strftime('%Y%m%d')}.log") file_handler = logging.FileHandler(log_file, encoding='utf-8') file_handler.setLevel(logging.DEBUG) # 文件里记录更详细的DEBUG信息 file_handler.setFormatter(formatter) logger.addHandler(file_handler) return logger

BasePage__init__方法中,使用self.logger = setup_logger(__name__)来初始化日志器。

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

在实际搭建和运行过程中,你一定会遇到各种各样的问题。这里记录了一些典型问题及其解决方案。

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

问题现象NoSuchElementException,TimeoutException,脚本报错说找不到元素。

排查思路与解决方案

  1. 等待问题(最常见)

    • 症状:页面还没加载完,脚本就去查找元素。
    • 解决:优先使用WebDriverWait配合expected_conditions(如presence_of_element_located,element_to_be_clickable)。减少或避免使用time.sleep()
    • 技巧:在BasePagefind_element方法中已经集成了显式等待,这是第一道防线。
  2. 定位器问题

    • 症状:元素ID、Class或XPath变了,或者页面上有多个相似元素。
    • 解决
      • 使用浏览器开发者工具:在Elements面板右键元素,Copy -> Copy selector / Copy XPath。但要注意自动生成的XPath可能很脆弱。
      • 优先使用稳定的属性id>name>class>textid通常是唯一且最稳定的。
      • 使用相对XPath或CSS Selector:避免使用绝对路径(如/html/body/div[7]/div[2]/...),它们极易因页面结构微小变动而失效。使用基于属性、文本或层级关系的相对路径。
      • 示例//button[contains(@class, ‘submit-btn’)]//*[@id=“app”]/div/div[2]/button要好得多。
  3. 元素在iframe或Shadow DOM中

    • 症状:在普通DOM里死活找不到。
    • 解决
      • iframe:使用driver.switch_to.frame(frame_reference)切换到iframe内部,操作完后再用driver.switch_to.default_content()切回来。
      • Shadow DOM:Selenium 4提供了对Shadow DOM的支持,可以使用driver.execute_script执行JavaScript来穿透Shadow Root查找元素。
  4. 页面弹窗或动态内容

    • 症状:操作后弹出一个提示框,挡住了后面的元素。
    • 解决:在操作后,显式等待弹窗出现并处理它(如点击确认),或者使用driver.switch_to.alert处理JavaScript原生弹窗。

实操心得:当元素定位失败时,不要盲目修改代码。首先,在脚本报错的那一行,手动打印出当前的页面URL和页面源代码(driver.page_source),看看元素是否真的在页面上。其次,在浏览器开发者工具的Console里尝试用JavaScript验证你的定位器是否有效,例如$x(‘你的XPath’)document.querySelector(‘你的CSS Selector’)

5.2 测试不稳定(Flaky Tests)

问题现象:同一个测试用例,有时能过,有时失败,没有规律。

排查思路与解决方案

  1. 异步加载与网络延迟:这是不稳定的主因。确保对所有依赖于网络请求或动态渲染的元素都使用了足够的、合适的等待。
  2. 依赖外部状态:测试用例依赖于一个特定的、可能变化的数据状态(如数据库里只有一条特定记录)。尽量让每个测试用例独立,使用测试数据准备和清理机制(setUp/tearDown或Pytest的fixture)。
  3. 浏览器窗口/标签页变化:某些操作(如点击一个链接)可能会打开新窗口或标签页。需要使用driver.switch_to.window(driver.window_handles[-1])来切换到新窗口,操作完再切回来。
  4. 使用重试机制:对于某些非核心的、已知偶尔会因环境问题失败的检查点,可以使用重试装饰器(如pytest-rerunfailures插件)。

5.3 如何组织大量测试用例并选择性地运行?

当有成百上千个测试用例时,你需要对它们进行分类和管理。

  1. 使用Pytest标记(Markers)

    # conftest.py 中注册自定义标记 def pytest_configure(config): config.addinivalue_line( "markers", "smoke: 冒烟测试用例" ) config.addinivalue_line( "markers", "regression: 回归测试用例" ) # 在测试用例上打标记 # test_cases/test_login.py import pytest @pytest.mark.smoke def test_login_success(self, login_page): pass @pytest.mark.regression def test_login_failure(self, login_page): pass

    然后可以只运行冒烟测试:pytest -m smoke

  2. 按目录或文件名运行pytest test_cases/运行整个目录。pytest test_cases/test_login.py运行单个文件。pytest test_cases/test_login.py::TestLogin::test_login_success运行单个测试函数。

  3. 使用pytest.ini配置文件:可以配置默认的测试路径、命令行参数等。

5.4 在CI/CD流水线中集成自动化测试

框架的最终价值在于持续集成。你可以将测试框架集成到Jenkins、GitLab CI、GitHub Actions等工具中。

核心步骤

  1. 环境准备:在CI服务器上安装Python、项目依赖(通过requirements.txt)、浏览器(如Chrome)和对应的驱动(可使用webdriver-manager自动处理)。
  2. 执行测试:在CI脚本中执行命令,如pytest --alluredir=./allure-results。通常使用无头模式(--headless)以节省资源。
  3. 收集结果:将测试结果(如allure-results目录、日志、截图)作为构建产物保存。
  4. 生成与展示报告:在构建后步骤中,调用allure generate生成HTML报告,并发布到CI服务器或专门的报告服务器上。

一个简单的GitHub Actions工作流示例(.github/workflows/test.yml):

name: Python Selenium Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.10' - name: Install dependencies run: | pip install -r requirements.txt sudo apt-get update sudo apt-get install -y chromium-browser - name: Run tests with pytest run: | pytest -v --alluredir=./allure-results - name: Upload Allure report uses: actions/upload-artifact@v3 with: name: allure-report path: ./allure-results

搭建一个自动化测试框架,从最初的几个脚本到如今这个结构清晰、功能完备的体系,最大的体会是**“磨刀不误砍柴工”**。前期在架构设计、基础封装上多花一点时间,后期在编写新用例、维护旧用例、排查问题时节省的时间是成倍的。这个框架的每个部分——POM、Fixture、数据驱动、日志报告——都不是炫技,而是为了解决实际工程中的痛点。当你下次再面对一个需要自动化的新功能时,你会发现大部分基础工作已经就绪,你只需要专注于编写新的页面对象和业务测试逻辑,效率和质量都得到了保障。最后一个小技巧:定期回顾和重构你的框架代码,随着项目和技术的发展,你可能会发现更优雅的实现方式,保持框架的活力同样重要。