Python UI自动化测试实战:pytest与Selenium黄金组合搭建企业级框架

发布时间:2026/6/19 5:12:18
Python UI自动化测试实战:pytest与Selenium黄金组合搭建企业级框架 1. 项目概述为什么选择 pytest selenium 这套组合拳如果你正在为网页应用的功能回归测试而头疼每次上线前都要手动点一遍按钮、填一遍表单那 UI 自动化测试就是你必须要掌握的一项技能。而pytest加selenium可以说是 Python 生态里做这件事的“黄金搭档”。我用了这套组合好几年从简单的登录测试到复杂的电商下单流程它都能稳稳地扛下来。简单来说selenium负责“动手”它就像一个虚拟的、不知疲倦的用户能按照你的指令去打开浏览器、点击元素、输入文字。而pytest负责“动脑”和“管理”它提供了强大的测试发现、执行、断言和报告机制让一堆零散的自动化操作变成结构清晰、可维护的测试用例集。市面上也有其他框架比如 unittest但 pytest 的简洁语法比如直接用assert、丰富的插件生态如生成漂亮的 HTML 报告以及强大的 fixture 机制让它成为更现代、更高效的选择。这套组合特别适合测试、开发以及 DevOps 工程师用于构建可靠的前端功能回归防线尤其是在敏捷开发、持续集成CI的流程中能极大解放人力。2. 环境搭建与核心工具选型开始写脚本之前得先把“战场”布置好。这里面的坑新手最容易踩。2.1 Python 环境与包管理首先确保你有一个干净的 Python 环境建议 3.7 及以上版本。我强烈推荐使用virtualenv或venv创建虚拟环境这是避免包冲突的黄金法则。在项目根目录下执行python -m venv venv # Windows 激活 venv\Scripts\activate # Linux/Mac 激活 source venv/bin/activate激活后命令行提示符前会出现(venv)标识。接着用 pip 安装核心包pip install pytest selenium这里有个实操心得网络问题可能导致 pip 安装缓慢或失败。可以配置国内镜像源例如使用阿里云镜像pip install -i https://mirrors.aliyun.com/pypi/simple/ pytest selenium。安装后用pytest --version和python -c “import selenium; print(selenium.__version__)”验证安装是否成功。2.2 浏览器驱动的选择与配置Selenium 本身不能直接控制浏览器它需要通过一个名为“WebDriver”的桥梁。你需要下载与你本地浏览器版本匹配的驱动。确定浏览器版本打开你的 Chrome 或 Firefox在设置里查看精确版本号。下载对应驱动Chrome:访问 ChromeDriver 官网 或国内镜像站下载对应版本。Firefox:访问 GeckoDriver 发布页 。配置驱动路径有三种常用方法我推荐第三种最省事。方法一不推荐将下载的驱动文件如chromedriver.exe放在系统 PATH 环境变量包含的目录里如 Windows 的C:\Windows。这容易造成版本管理混乱。方法二灵活在代码中指定驱动路径。from selenium import webdriver driver webdriver.Chrome(executable_path‘/你的路径/chromedriver’)方法三推荐使用webdriver-manager安装这个包它可以自动下载和管理匹配的浏览器驱动彻底告别手动下载和版本匹配的烦恼。pip install webdriver-manager使用时from selenium import webdriver from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.chrome.service import Service service Service(ChromeDriverManager().install()) driver webdriver.Chrome(serviceservice)注意如果你在公司内网或代理环境下webdriver-manager可能下载失败。这时需要回退到方法二并确保团队内部共享统一版本的驱动文件。2.3 IDE 与项目结构规划用什么写代码都行但 PyCharm 或 VS Code 对 Python 和 pytest 的支持更好。更重要的是规划一个清晰的项目结构这对后续维护至关重要。一个典型的结构如下your_ui_auto_project/ ├── conftest.py # pytest 共享 fixture 配置如驱动初始化 ├── requirements.txt # 项目依赖包列表 ├── test_cases/ # 存放测试用例文件 │ ├── __init__.py │ ├── test_login.py │ └── test_search.py ├── page_objects/ # 页面对象模型PO目录 │ ├── __init__.py │ ├── base_page.py │ ├── login_page.py │ └── home_page.py ├── test_data/ # 测试数据文件如 JSON, YAML, Excel │ └── users.json ├── reports/ # 测试报告输出目录由插件生成 └── utils/ # 工具函数如截图、日志、数据读取 ├── __init__.py └── logger.py先建立这个骨架后面的代码往里填思路会清晰很多。3. 从零编写第一个自动化脚本与用例让我们从一个最简单的例子开始打开百度搜索一个关键词并验证搜索结果标题。3.1 最简单的线性脚本创建一个文件test_first_script.py先不用任何框架就用最原始的 Selenium 脚本from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys import time # 1. 启动浏览器这里使用自动管理驱动的方式 from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.chrome.service import Service service Service(ChromeDriverManager().install()) driver webdriver.Chrome(serviceservice) try: # 2. 打开网页 driver.get(“https://www.baidu.com”) time.sleep(2) # 等待页面加载这是初级做法后面会改进 # 3. 定位元素并操作 search_box driver.find_element(By.ID, “kw”) # 百度搜索框的ID是‘kw’ search_box.send_keys(“pytest selenium”) search_box.send_keys(Keys.RETURN) # 模拟回车键 time.sleep(3) # 等待搜索结果加载 # 4. 进行断言验证 assert “pytest_selenium” in driver.title.lower() # 粗略验证标题 print(“测试通过”) finally: # 5. 关闭浏览器 driver.quit()运行这个脚本python test_first_script.py。你会看到浏览器自动打开、搜索、然后关闭。这就是自动化的魔力。但这段代码问题很多硬编码的等待time.sleep、断言过于简单、没有错误报告、浏览器窗口一闪而过不利于调试。3.2 用 pytest 改造为正式测试用例现在我们用 pytest 的规则重写它。pytest 会自动发现以test_开头或_test结尾的文件和函数。创建test_cases/test_baidu_search.pyimport pytest from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class TestBaiduSearch: 百度搜索测试类 # 每个测试方法开始前执行 def setup_method(self): from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.chrome.service import Service service Service(ChromeDriverManager().install()) self.driver webdriver.Chrome(serviceservice) self.driver.implicitly_wait(10) # 设置隐式等待全局生效 self.wait WebDriverWait(self.driver, 10) # 显式等待对象 # 每个测试方法结束后执行 def teardown_method(self): self.driver.quit() def test_search_pytest_selenium(self): 测试搜索 pytest selenium 关键字 driver self.driver wait self.wait driver.get(“https://www.baidu.com”) # 使用显式等待更智能地等待元素出现 search_input wait.until( EC.presence_of_element_located((By.ID, “kw”)) ) search_input.send_keys(“pytest selenium” Keys.RETURN) # 等待搜索结果区域出现并断言其中包含特定文本 first_result wait.until( EC.presence_of_element_located((By.CSS_SELECTOR, “#content_left .result”)) ) assert “pytest” in first_result.text.lower() assert “selenium” in first_result.text.lower() # 你可以添加更多测试方法 def test_search_weather(self): 测试搜索天气 self.driver.get(“https://www.baidu.com”) # ... 类似的操作和断言现在在项目根目录下运行pytest test_cases/test_baidu_search.py -v。-v参数表示详细输出你会看到每个测试方法的执行结果PASSED 或 FAILED。pytest 会自动执行setup_method和teardown_method管理浏览器的生命周期。这里的核心改进是用了等待机制替代了不稳定的time.sleep。4. 构建可维护的自动化测试框架当用例越来越多直接在每个测试方法里写find_element和send_keys会变得难以维护。这时需要引入设计模式。4.1 页面对象模型PO实战PO 模式的核心思想是将页面封装成类页面的元素定位和操作作为类的方法测试用例只调用这些方法不关心具体定位细节。这样前端页面改了你只需要改对应的 Page 类测试用例基本不用动。首先在page_objects目录下创建base_page.py这是一个所有页面类的基类封装公共操作from selenium.webdriver.support.wait import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class BasePage: def __init__(self, driver): self.driver driver self.wait WebDriverWait(driver, 10) def find_element(self, locator): 查找单个元素显式等待 return self.wait.until(EC.presence_of_element_located(locator)) def find_elements(self, locator): 查找多个元素 return self.wait.until(EC.presence_of_all_elements_located(locator)) def click(self, locator): 点击元素 element self.find_element(locator) element.click() def input_text(self, locator, text): 输入文本 element self.find_element(locator) element.clear() element.send_keys(text) def get_text(self, locator): 获取元素文本 return self.find_element(locator).text然后创建具体的页面类例如login_page.pyfrom selenium.webdriver.common.by import By from .base_page import BasePage class LoginPage(BasePage): # 定位器将元素定位方式集中管理 USERNAME_INPUT (By.ID, “username”) PASSWORD_INPUT (By.ID, “password”) LOGIN_BUTTON (By.ID, “loginBtn”) ERROR_MSG (By.CLASS_NAME, “error-message”) def __init__(self, driver): super().__init__(driver) self.driver driver def open(self, url): self.driver.get(url) return self def login(self, username, password): 登录操作 self.input_text(self.USERNAME_INPUT, username) self.input_text(self.PASSWORD_INPUT, password) self.click(self.LOGIN_BUTTON) return self # 链式调用可返回结果页对象 def get_error_message(self): 获取错误提示信息 return self.get_text(self.ERROR_MSG)最后测试用例变得非常简洁清晰。创建test_cases/test_login.pyimport pytest from page_objects.login_page import LoginPage class TestLogin: def test_login_success(self, browser): # 使用了 fixture ‘browser‘ login_page LoginPage(browser) login_page.open(“https://your-app.com/login”) # 假设登录成功会跳转到首页首页标题包含‘Dashboard’ login_page.login(“valid_user”, “valid_pass”) assert “Dashboard” in browser.title def test_login_failed_with_wrong_password(self, browser): login_page LoginPage(browser) login_page.open(“https://your-app.com/login”) login_page.login(“valid_user”, “wrong_pass”) error_text login_page.get_error_message() assert “密码错误” in error_text你看测试用例里已经没有find_element和By.ID了全是业务语义的操作。这就是 PO 模式带来的可读性和可维护性提升。4.2 使用 pytest fixture 管理测试生命周期上面的例子中TestLogin类需要一个browser参数。这个browser就是一个fixture它负责创建和销毁 WebDriver 实例。我们把fixture定义在conftest.py文件中这个文件里的fixture可以被整个项目共享。在项目根目录创建conftest.pyimport pytest from selenium import webdriver from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.chrome.service import Service pytest.fixture(scope“class”) def browser(): 提供 WebDriver 实例的 fixture作用域为类级别一个测试类共用同一个浏览器实例 service Service(ChromeDriverManager().install()) driver webdriver.Chrome(serviceservice) driver.implicitly_wait(5) driver.maximize_window() # 默认最大化窗口 yield driver # 测试执行时使用 driver driver.quit() # 测试结束后退出 pytest.fixture def login_page(browser): 直接提供一个登录页面的 fixture from page_objects.login_page import LoginPage return LoginPage(browser)现在测试用例可以直接使用这些fixture。scope“class”表示这个browser在一个测试类中只初始化一次所有方法共用可以加快执行速度如果用例间没有状态依赖。你也可以用scope“function”默认值让每个测试方法都重启浏览器。4.3 测试数据分离与管理不要把测试数据硬编码在用例里。将数据分离出来便于管理和参数化测试。pytest 的pytest.mark.parametrize装饰器是神器。首先可以简单地在用例中参数化import pytest class TestLoginWithData: pytest.mark.parametrize(“username, password, expected”, [ (“admin”, “admin123”, True), # 成功用例 (“admin”, “wrong”, False), # 失败用例 (“”, “admin123”, False), # 空用户名 (“admin”, “”, False), # 空密码 ]) def test_login_params(self, browser, username, password, expected): login_page LoginPage(browser) login_page.open(“...”) login_page.login(username, password) if expected: assert “Dashboard” in browser.title else: assert login_page.get_error_message() ! “”对于更复杂的数据可以放在外部文件里如 JSON 或 YAML。创建test_data/login_data.json[ { “case_name”: “正确账号密码登录成功”, “username”: “test_user”, “password”: “123456”, “expected”: “success” }, { “case_name”: “错误密码登录失败”, “username”: “test_user”, “password”: “wrong”, “expected”: “fail” } ]然后在conftest.py或工具函数中读取数据import json import pytest def load_login_data(): with open(‘test_data/login_data.json’, ‘r’, encoding‘utf-8’) as f: return json.load(f) pytest.fixture(paramsload_login_data()) def login_data(request): return request.param # 在用例中使用 def test_login_with_json_data(browser, login_data): # login_data 就是 JSON 数组中的每一个字典对象 pass5. 高级技巧与最佳实践掌握了基础框架后这些技巧能让你的自动化脚本更健壮、更专业。5.1 智能等待与元素定位策略永远不要用time.sleep这是 UI 自动化测试的第一条军规。要用 Selenium 提供的等待机制。隐式等待 (Implicit Wait)driver.implicitly_wait(10)设置一个全局等待时间在查找任何元素时如果元素没有立即出现会轮询等待最多10秒。它是一次性设置对整个 driver 生命周期有效。但它不适用于元素的状态如可点击、可见。显式等待 (Explicit Wait)针对特定元素和条件进行等待更灵活、更精确。这是推荐的主要方式。from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By wait WebDriverWait(driver, 10, poll_frequency0.5, ignored_exceptions[NoSuchElementException]) # 等待元素可见并可点击 button wait.until(EC.element_to_be_clickable((By.ID, “submitBtn”))) button.click() # 等待元素包含特定文本 element wait.until(EC.text_to_be_present_in_element((By.TAG_NAME, “h1”), “Welcome”))元素定位优先级建议IDNameCSS SelectorXPath。ID 和 Name 通常最稳定。CSS Selector 性能好语法简洁。XPath 功能强大但性能稍差且容易因页面结构微小变动而失效谨慎使用。对于动态ID包含变化部分可以使用CSS Selector的模糊匹配如input[id*‘username’]。5.2 测试报告与日志记录跑完测试你需要知道结果。pytest 有很多插件可以生成漂亮的报告。pytest-html生成 HTML 报告。pip install pytest-html pytest --htmlreports/report.html --self-contained-htmlallure-pytest生成非常美观、交互性强的 Allure 报告。pip install allure-pytest pytest --alluredir./allure-results # 生成报告 allure serve ./allure-results同时加入日志记录方便调试。在conftest.py或一个工具模块中配置import logging import sys def get_logger(name): logger logging.getLogger(name) logger.setLevel(logging.INFO) if not logger.handlers: ch logging.StreamHandler(sys.stdout) formatter logging.Formatter(‘%(asctime)s - %(name)s - %(levelname)s - %(message)s’) ch.setFormatter(formatter) logger.addHandler(ch) return logger # 在 fixture 或 Page 类中使用 logger get_logger(__name__) logger.info(“正在打开登录页面...”)5.3 失败截图与重试机制测试失败时一张截图抵得上千行日志。我们可以通过修改conftest.py中的 fixture 或使用 hook 函数来实现自动截图。import pytest from datetime import datetime pytest.hookimpl(tryfirstTrue, hookwrapperTrue) def pytest_runtest_makereport(item, call): 获取测试用例执行结果的钩子函数 outcome yield rep outcome.get_result() if rep.when “call” and rep.failed: # 只有测试执行阶段失败才截图 driver item.funcargs.get(“browser”, None) if driver is not None: timestamp datetime.now().strftime(“%Y%m%d_%H%M%S”) screenshot_name f”screenshot_failure_{item.name}_{timestamp}.png” driver.save_screenshot(f”reports/{screenshot_name}”) print(f”\n测试失败截图已保存至reports/{screenshot_name}”)对于不稳定的测试如网络波动可以使用pytest-rerunfailures插件进行重试。pip install pytest-rerunfailures pytest --reruns 3 --reruns-delay 2 # 失败后重试3次每次间隔2秒6. 常见问题排查与避坑指南在实际操作中你肯定会遇到各种奇怪的问题。这里记录了一些高频坑点和解决思路。6.1 元素定位不到NoSuchElementException这是最常见的问题。可能原因1等待时间不足。页面还没加载完就去定位元素。解决使用显式等待WebDriverWait配合EC.presence_of_element_located或EC.visibility_of_element_located。可能原因2元素在 iframe 或 shadow DOM 内。解决先切换到对应的 iframedriver.switch_to.frame(“frame_name_or_id”)操作完再切回来driver.switch_to.default_content()。Shadow DOM 需要使用driver.execute_script执行 JavaScript 来定位。可能原因3元素属性是动态生成的。每次刷新页面 ID 或 Class 会变。解决使用相对定位如通过父元素的稳定属性结合 XPath 轴如following-sibling::,parent::或 CSS Selector 的其他属性如^开头$结尾*包含。可能原因4页面有多个相同属性的元素。find_element只返回第一个。解决使用find_elements获取列表然后按索引或遍历查找或者使用更精确的定位器。6.2 脚本在 CI/CD 环境如 Jenkins中运行失败本地跑得好好的一上 Jenkins 就挂。可能原因1无头模式或缺少显示服务器。Linux 服务器通常没有图形界面。解决使用无头模式运行 Chrome。from selenium.webdriver.chrome.options import Options chrome_options Options() chrome_options.add_argument(“--headless”) # 无头模式 chrome_options.add_argument(“--no-sandbox”) # 在 Docker 或某些 CI 环境中需要 chrome_options.add_argument(“--disable-dev-shm-usage”) # 解决共享内存问题 driver webdriver.Chrome(optionschrome_options)可能原因2环境路径问题。Jenkins 节点上可能没有 Chrome 浏览器或驱动。解决确保 CI 环境中安装了 Chrome或使用webdriver-manager自动处理驱动或者使用 Docker 镜像来提供一致的环境。可能原因3权限或防火墙。脚本无法访问目标测试环境。解决检查 Jenkins 节点的网络配置确保能连通测试服务器。6.3 自动化测试不稳定Flaky Tests有时成功有时失败最让人抓狂。对策1强化等待。检查所有关键操作前后是否都有合适的显式等待特别是对于 Ajax 加载的内容。对策2避免依赖固定等待时间。彻底抛弃time.sleep。对策3优化定位器。使用更稳定、唯一的元素定位方式避免使用绝对 XPath。对策4引入重试机制。如上文所述使用pytest-rerunfailures对不稳定用例进行重试。对策5隔离测试环境与数据。确保测试用例是独立的不依赖前一个用例产生的数据。每次测试前可以清理或重置测试数据。6.4 如何选择 UI 自动化还是接口自动化这也是一个常见困惑。我的经验是UI 自动化适合验证端到端的用户业务流程和前端交互。例如完整的用户注册-登录-下单流程。它更贴近真实用户但运行慢、稳定性相对差、维护成本高。接口自动化适合验证业务逻辑和数据一致性。它运行极快、稳定性高、维护成本低。例如验证提交订单接口是否成功扣减库存、生成订单号。最佳实践是结合使用用接口自动化覆盖大部分业务逻辑和核心数据流测试构建快速反馈的测试层用 UI 自动化覆盖关键的用户场景和核心业务流程作为最终的用户验收层。不要试图用 UI 自动化覆盖所有测试点那会是一场维护噩梦。我个人在搭建自动化体系时通常会遵循“金字塔模型”底层是大量的单元测试和接口测试快速稳定顶层是少量的、关键的 UI 自动化测试覆盖核心场景。pytest selenium 正是打造这顶层关键测试的利器。记住UI 自动化的目标不是发现大量 bug而是保障核心流程的畅通无阻为持续交付提供信心。