前言:Hello 大家好!我是励志死磕计算机 ~ 前面3篇我们已经掌握了UI自动化的基础技能:环境搭建、元素定位、核心操作。但相信大家在练习中已经发现了问题------写的脚本都是零散的,改一个元素定位要在多个脚本里改,无法批量执行,也没有测试报告。这就是新手和企业级自动化的核心差距:是否有规范的框架支撑。今天这篇文章,我们就从"写脚本"升级到"做项目",手把手教大家用Pytest测试框架+PO模式,搭建一个可复用、易维护、可扩展的企业级UI自动化基础框架,全程代码实战,复制就能用!
本文核心目标:
-
理解企业级自动化框架的核心价值,搞懂"为什么需要框架"
-
熟练掌握Pytest测试框架核心用法(Fixture、参数化、测试报告)
-
理解并落地PO模式(Page Object Model)分层设计思想
-
从0到1搭建完整框架,包含页面对象层、测试用例层、工具层、配置层
-
掌握框架运行与问题排查技巧,适配企业真实测试场景
一、先搞懂:为什么需要企业级框架?
在开始搭建之前,我们先明确"框架"的核心作用。新手写的自动化脚本,通常会遇到以下4个痛点,而框架就是为了解决这些问题而生的!
1. 新手零散脚本的4大痛点
-
代码冗余:多个脚本重复写驱动初始化、元素定位、操作步骤,改一处要改多处
-
维护困难:页面元素变更后,需要在所有用到该元素的脚本中修改,成本极高
-
无法批量执行:只能单个脚本运行,无法一次性执行所有用例,效率低下
-
无测试报告:运行结果需要手动查看控制台,无法生成直观的报告供团队查看
2. 企业级框架的核心要求
一个合格的企业级UI自动化框架,必须满足"可复用、易维护、可扩展、可监控"4个核心要求:
-
可复用:常用操作(如驱动初始化、登录、截图)封装成公共方法,多个用例直接调用
-
易维护:页面元素和操作集中管理,元素变更只需修改一处,不影响用例
-
可扩展:支持新增页面、新增用例时快速集成,无需重构整个框架
-
可监控:能批量执行用例,生成详细测试报告,支持失败重试、自动截图,方便问题定位
3. 为什么选Pytest+PO模式?
这是目前企业中最主流的UI自动化框架组合,优势互补:
-
Pytest:Python生态中最强大的测试框架,支持Fixture前置后置、参数化、测试报告、失败重试等企业级特性,比Python自带的unittest更灵活、易用
-
PO模式:页面对象模型(Page Object Model),核心是"页面元素与操作封装、测试用例与页面分离",从设计层面解决代码冗余和维护困难的问题
二、前置准备:环境依赖安装
搭建框架前,先安装所需的依赖库,打开cmd/终端执行以下命令(建议创建虚拟环境,避免依赖冲突):
# 安装Pytest(测试框架核心)
pip install pytest==7.4.0 # 指定稳定版本,避免兼容性问题
# 安装Pytest-HTML(生成基础测试报告)
pip install pytest-html==3.2.0
# 安装Selenium(UI自动化核心)
pip install selenium==4.11.2
# 安装webdriver-manager(自动管理浏览器驱动)
pip install webdriver-manager==4.0.0
# 安装python-dotenv(读取配置文件)
pip install python-dotenv==1.0.0
验证安装成功:执行pytest --version,若输出Pytest版本信息(如pytest 7.4.0),则说明安装成功。
三、Pytest测试框架核心用法(企业级必备)
Pytest是框架的"骨架",负责用例管理、执行、报告生成等核心功能。我们先掌握最关键的4个特性,这是搭建框架的基础。
1. 测试用例规范(必须遵守)
Pytest有严格的用例命名规范,不遵守则无法识别用例:
-
测试文件:以
test_开头(如test_login.py)或_test结尾(如login_test.py) -
测试类:以
Test开头,且不能有__init__方法(如TestLogin) -
测试方法/函数:以
test_开头(如test_login_success) -
断言:使用Python原生的
assert语句(如assert "登录成功" in driver.page_source)
示例(符合规范的用例):
# 文件名:test_login.py
class TestLogin:
def test_login_success(self):
# 测试登录成功场景
assert "登录成功" in "当前页面显示:登录成功"
def test_login_fail(self):
# 测试登录失败场景
assert "用户名错误" in "当前页面显示:用户名错误"
2. Fixture:前置/后置操作的"万能工具"
Fixture是Pytest的核心特性,用于封装前置操作(如驱动初始化、打开浏览器)和后置操作(如关闭浏览器、清理数据),替代传统的setup/teardown,支持全局复用、作用域控制。
(1)基础用法:定义与调用
创建conftest.py文件(Pytest会自动识别该文件中的Fixture,无需导入):
# 文件名:conftest.py
import pytest
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager
# 定义Fixture,作用域为function(默认),每个测试函数执行前都会调用
@pytest.fixture(scope="function")
def init_driver():
# 前置操作:初始化驱动、打开浏览器
driver = webdriver.Chrome(ChromeDriverManager().install())
driver.maximize_window() # 最大化窗口
driver.implicitly_wait(10) # 隐式等待10秒
yield driver # yield之前是前置操作,之后是后置操作
# 后置操作:关闭浏览器
driver.quit()
在测试用例中调用Fixture(直接作为参数传入):
# 文件名:test_login.py
class TestLogin:
# 直接将Fixture名作为参数传入,Pytest会自动执行前置操作
def test_login_success(self, init_driver):
driver = init_driver
# 访问登录页
driver.get("https://xxx.com/login")
# 输入账号密码
driver.find_element_by_id("username").send_keys("test_user")
driver.find_element_by_id("password").send_keys("test_pass")
driver.find_element_by_id("login_btn").click()
# 断言登录成功
assert "欢迎您,test_user" in driver.page_source
(2)Fixture作用域(关键)
通过scope参数控制Fixture的作用范围,企业级框架常用3种:
-
scope="function":默认值,每个测试函数执行前都调用(最常用,保证用例隔离) -
scope="class":每个测试类执行前调用一次 -
scope="module":每个测试文件执行前调用一次
示例(类级作用域):
@pytest.fixture(scope="class")
def init_class_driver():
# 前置操作:初始化驱动
driver = webdriver.Chrome(ChromeDriverManager().install())
driver.maximize_window()
driver.implicitly_wait(10)
yield driver
# 后置操作:关闭驱动
driver.quit()
3. 参数化:多组数据批量执行用例
企业测试中经常需要用多组数据验证同一功能(如不同账号登录、不同参数输入),Pytest的@pytest.mark.parametrize装饰器可实现参数化,批量执行用例。
示例(多账号登录测试):
# 文件名:test_login.py
import pytest
class TestLogin:
# 参数化装饰器:第一个参数是参数名(可多个),第二个参数是测试数据(列表/元组)
@pytest.mark.parametrize("username, password, expected_result", [
("test_user1", "test_pass1", "欢迎您,test_user1"), # 正常登录
("test_user2", "wrong_pass", "用户名或密码错误"), # 密码错误
("wrong_user", "test_pass3", "用户名或密码错误"), # 用户名错误
("", "", "请输入用户名") # 空值校验
])
def test_login_parametrize(self, init_driver, username, password, expected_result):
driver = init_driver
driver.get("https://xxx.com/login")
# 输入参数化的账号密码
driver.find_element_by_id("username").send_keys(username)
driver.find_element_by_id("password").send_keys(password)
driver.find_element_by_id("login_btn").click()
# 断言预期结果
assert expected_result in driver.page_source
运行后会自动生成4条测试用例,分别执行4组数据,效率大幅提升!
4. 测试报告:pytest-html生成直观报告
企业级测试需要直观的报告展示用例执行结果,pytest-html可生成HTML格式的测试报告,支持显示用例详情、失败原因、截图等。
(1)生成基础报告
在cmd/终端中进入项目目录,执行以下命令:
# -v:详细输出用例执行信息
# -s:打印用例中的print日志
# --html=report.html:生成名为report.html的测试报告,存放在当前目录
pytest -v -s --html=report.html test_login.py
执行完成后,项目目录会生成report.html文件,用浏览器打开即可查看报告,包含用例通过率、执行时间、失败详情等信息。
(2)报告优化:添加截图(失败自动截图)
在conftest.py中添加失败截图逻辑,用例失败时自动保存截图并嵌入报告:
# 文件名:conftest.py
import pytest
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager
from datetime import datetime
import os
# 定义截图保存路径
SCREENSHOT_DIR = "./screenshots"
if not os.path.exists(SCREENSHOT_DIR):
os.makedirs(SCREENSHOT_DIR)
@pytest.fixture(scope="function")
def init_driver(request): # 添加request参数,用于获取用例信息
driver = webdriver.Chrome(ChromeDriverManager().install())
driver.maximize_window()
driver.implicitly_wait(10)
# 定义后置操作:失败截图
def fin():
# 判断用例是否失败
if request.node.rep_call.failed:
# 生成截图文件名(用例名+时间戳)
timestamp = datetime.strftime(datetime.now(), "%Y%m%d%H%M%S")
screenshot_name = f"{request.node.name}_{timestamp}.png"
screenshot_path = os.path.join(SCREENSHOT_DIR, screenshot_name)
# 保存截图
driver.save_screenshot(screenshot_path)
print(f"用例失败,截图已保存至:{screenshot_path}")
# 注册后置操作
request.addfinalizer(fin)
yield driver
driver.quit()
# 用于捕获用例执行结果(必须添加,否则无法判断用例是否失败)
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
rep = outcome.get_result()
# 存储用例执行结果到item对象中
setattr(item, "rep_call", rep)
再次运行生成报告的命令,失败用例会自动保存截图,打开报告后可直接查看截图,方便问题定位。
四、PO模式:企业级框架的"灵魂"设计
PO模式(Page Object Model)是一种设计模式,核心思想是"将每个页面封装成一个对象,页面的元素和操作都集中在该对象中,测试用例只调用页面对象的方法,不直接操作元素"。这样可以实现"页面与用例分离",大幅降低维护成本。
1. PO模式三大核心原则
-
页面元素与操作封装:每个页面的元素(如id、XPath)和操作(如输入账号、点击登录)都封装在对应的页面对象类中
-
测试用例与页面分离:测试用例不关心页面的具体元素和操作细节,只调用页面对象的方法完成流程
-
复用性优先:公共页面(如导航栏、 Footer)可封装成公共页面对象,供其他页面复用
2. PO模式四层架构(企业标准)
搭建框架时,我们采用四层架构设计,目录结构清晰,职责明确:
ui_auto_framework/ # 项目根目录
├── config/ # 配置层:存储全局配置(URL、账号、等待时间等)
│ └── config.py # 配置文件
├── pages/ # 页面对象层:封装各页面的元素和操作
│ ├── base_page.py # 基础页面(封装公共操作,如等待、截图)
│ ├── login_page.py# 登录页面对象
│ └── home_page.py # 首页面对象
├── tests/ # 测试用例层:编写测试用例
│ └── test_login.py# 登录相关测试用例
├── utils/ # 工具层:封装公共工具(日志、截图、读取配置等)
│ └── logger.py # 日志工具
├── conftest.py # Pytest Fixture配置文件
└── requirements.txt # 项目依赖库清单
3. 各层实现实战(核心步骤)
下面我们逐一实现各层代码,以"登录页→首页"的流程为例。
(1)配置层:config/config.py
存储全局配置信息,避免硬编码(如URL、账号密码),方便后续修改:
# 文件名:config/config.py
# 全局配置信息
class Config:
# 基础URL
BASE_URL = "https://xxx.com"
# 登录页URL
LOGIN_URL = f"{BASE_URL}/login"
# 首页URL
HOME_URL = f"{BASE_URL}/home"
# 测试账号密码
TEST_USERNAME = "test_user"
TEST_PASSWORD = "test_pass"
# 等待时间(秒)
IMPLICITLY_WAIT = 10
EXPLICITLY_WAIT = 15
# 实例化配置对象,供其他模块调用
config = Config()
(2)工具层:utils/logger.py(日志工具)
封装日志工具,用于记录测试过程(如用例开始、操作步骤、失败异常),方便问题定位:
# 文件名:utils/logger.py
import logging
import os
from datetime import datetime
# 日志保存路径
LOG_DIR = "./logs"
if not os.path.exists(LOG_DIR):
os.makedirs(LOG_DIR)
# 日志文件名(时间戳)
LOG_FILE = f"{LOG_DIR}/test_log_{datetime.strftime(datetime.now(), '%Y%m%d')}.log"
# 配置日志格式
logging.basicConfig(
level=logging.INFO, # 日志级别:INFO及以上会被记录
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", # 日志格式
handlers=[
logging.FileHandler(LOG_FILE, encoding="utf-8"), # 写入文件
logging.StreamHandler() # 输出到控制台
]
)
# 创建日志对象,供其他模块调用
logger = logging.getLogger("UI_AUTO_LOG")
(3)页面对象层:base_page.py(基础页面)
封装所有页面的公共操作(如显式等待、点击、输入、截图),其他页面对象继承该类:
# 文件名: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
from utils.logger import logger
from config.config import config
class BasePage:
def __init__(self, driver):
self.driver = driver
self.base_url = config.BASE_URL
self.explicitly_wait = config.EXPLICITLY_WAIT
# 访问页面
def open(self, url):
logger.info(f"访问页面:{url}")
self.driver.get(url)
# 显式等待元素可点击
def wait_element_clickable(self, locator):
try:
element = WebDriverWait(self.driver, self.explicitly_wait).until(
EC.element_to_be_clickable(locator)
)
logger.info(f"元素可点击:{locator}")
return element
except TimeoutException:
logger.error(f"等待元素可点击超时:{locator}")
raise
# 显式等待元素可见
def wait_element_visible(self, locator):
try:
element = WebDriverWait(self.driver, self.explicitly_wait).until(
EC.visibility_of_element_located(locator)
)
logger.info(f"元素可见:{locator}")
return element
except TimeoutException:
logger.error(f"等待元素可见超时:{locator}")
raise
# 点击操作
def click(self, locator):
element = self.wait_element_clickable(locator)
element.click()
logger.info(f"点击元素:{locator}")
# 输入操作
def input_text(self, locator, text):
element = self.wait_element_visible(locator)
element.clear()
element.send_keys(text)
logger.info(f"向元素{locator}输入文本:{text}")
# 获取元素文本
def get_element_text(self, locator):
element = self.wait_element_visible(locator)
text = element.text
logger.info(f"获取元素{locator}文本:{text}")
return text
# 截图操作
def screenshot(self, filename):
screenshot_path = f"./screenshots/{filename}.png"
self.driver.save_screenshot(screenshot_path)
logger.info(f"截图保存至:{screenshot_path}")
return screenshot_path
(4)页面对象层:login_page.py(登录页面)
继承BasePage,封装登录页的专属元素和操作(如输入账号、输入密码、点击登录):
# 文件名:pages/login_page.py
from pages.base_page import BasePage
from selenium.webdriver.common.by import By
from config.config import config
class LoginPage(BasePage):
# 页面元素定位器(元组格式:(定位方式, 定位值))
USERNAME_INPUT = (By.ID, "username") # 账号输入框
PASSWORD_INPUT = (By.ID, "password") # 密码输入框
LOGIN_BUTTON = (By.ID, "login_btn") # 登录按钮
ERROR_MESSAGE = (By.CLASS_NAME, "error_msg") # 错误提示信息
WELCOME_MESSAGE = (By.ID, "welcome_msg") # 登录成功欢迎信息
# 登录操作(核心方法)
def login(self, username, password):
self.open(config.LOGIN_URL) # 访问登录页
self.input_text(self.USERNAME_INPUT, username) # 输入账号
self.input_text(self.PASSWORD_INPUT, password) # 输入密码
self.click(self.LOGIN_BUTTON) # 点击登录
# 获取错误提示信息
def get_error_message(self):
return self.get_element_text(self.ERROR_MESSAGE)
# 获取登录成功欢迎信息
def get_welcome_message(self):
return self.get_element_text(self.WELCOME_MESSAGE)
(5)页面对象层:home_page.py(首页)
封装首页的元素和操作,用于验证登录后跳转是否正确:
# 文件名:pages/home_page.py
from pages.base_page import BasePage
from selenium.webdriver.common.by import By
from config.config import config
class HomePage(BasePage):
# 页面元素定位器
USER_INFO = (By.ID, "user_info") # 用户信息展示区
LOGOUT_BUTTON = (By.ID, "logout_btn") # 退出登录按钮
# 验证是否进入首页
def is_home_page(self):
return config.HOME_URL in self.driver.current_url
# 获取当前登录用户名
def get_current_username(self):
return self.get_element_text(self.USER_INFO)
# 退出登录
def logout(self):
self.click(self.LOGOUT_BUTTON)
logger.info("执行退出登录操作")
(6)测试用例层:tests/test_login.py
调用页面对象的方法编写测试用例,不涉及任何元素定位细节:
# 文件名:tests/test_login.py
import pytest
from pages.login_page import LoginPage
from pages.home_page import HomePage
from config.config import config
from utils.logger import logger
class TestLogin:
# 测试登录成功场景
def test_login_success(self, init_driver):
logger.info("开始执行测试用例:test_login_success")
driver = init_driver
# 初始化页面对象
login_page = LoginPage(driver)
home_page = HomePage(driver)
# 执行登录操作(调用登录页对象的方法)
login_page.login(config.TEST_USERNAME, config.TEST_PASSWORD)
# 断言:是否跳转到首页
assert home_page.is_home_page(), "登录后未跳转到首页"
# 断言:首页显示当前用户名
assert config.TEST_USERNAME in home_page.get_current_username(), "首页未显示正确的用户名"
logger.info("测试用例:test_login_success 执行成功")
# 测试登录失败场景(参数化)
@pytest.mark.parametrize("username, password, expected_error", [
("test_user", "wrong_pass", "用户名或密码错误"),
("wrong_user", "test_pass", "用户名或密码错误"),
("", "", "请输入用户名")
])
def test_login_fail(self, init_driver, username, password, expected_error):
logger.info(f"开始执行测试用例:test_login_fail,参数:{username}, {password}")
driver = init_driver
login_page = LoginPage(driver)
# 执行登录操作
login_page.login(username, password)
# 断言:显示正确的错误提示
assert login_page.get_error_message() == expected_error, f"错误提示不正确,预期:{expected_error}"
logger.info(f"测试用例:test_login_fail({username})执行成功")
(7)Fixture配置:conftest.py
整合驱动初始化、日志、截图等功能,供用例调用:
# 文件名:conftest.py
import pytest
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager
from config.config import config
from utils.logger import logger
import os
from datetime import datetime
# 截图保存路径
SCREENSHOT_DIR = "./screenshots"
if not os.path.exists(SCREENSHOT_DIR):
os.makedirs(SCREENSHOT_DIR)
# 驱动初始化Fixture
@pytest.fixture(scope="function")
def init_driver(request):
logger.info("开始初始化浏览器驱动")
# 初始化Chrome驱动
driver = webdriver.Chrome(ChromeDriverManager().install())
driver.maximize_window()
driver.implicitly_wait(config.IMPLICITLY_WAIT)
logger.info("浏览器驱动初始化完成")
# 后置操作:失败截图+关闭驱动
def fin():
logger.info("开始执行后置操作")
# 用例失败截图
if request.node.rep_call.failed:
timestamp = datetime.strftime(datetime.now(), "%Y%m%d%H%M%S")
screenshot_name = f"{request.node.name}_{timestamp}"
driver.save_screenshot(f"{SCREENSHOT_DIR}/{screenshot_name}.png")
logger.error(f"用例执行失败,截图已保存:{screenshot_name}.png")
# 关闭驱动
driver.quit()
logger.info("浏览器驱动已关闭")
request.addfinalizer(fin)
yield driver
# 捕获用例执行结果
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
rep = outcome.get_result()
setattr(item, "rep_call", rep)
# 自定义命令行参数(可选,用于指定环境)
def pytest_addoption(parser):
parser.addoption("--env", action="store", default="test", help="指定测试环境:test/prod")
@pytest.fixture(scope="session")
def env(request):
return request.config.getoption("--env")
(8)依赖清单:requirements.txt
记录项目所有依赖库及版本,方便他人快速搭建环境:
pytest==7.4.0
pytest-html==3.2.0
selenium==4.11.2
webdriver-manager==4.0.0
python-dotenv==1.0.0
五、框架运行与问题排查
框架搭建完成后,我们学习如何运行用例、查看报告,以及常见问题的排查方法。
1. 框架运行命令
进入项目根目录,执行以下命令(根据需求选择):
# 1. 运行指定测试文件
pytest tests/test_login.py -v -s
# 2. 运行所有测试用例(tests目录下所有符合规范的用例)
pytest -v -s
# 3. 运行指定标记的用例(需先给用例加标记,如@pytest.mark.smoke)
pytest -v -s -m smoke
# 4. 生成测试报告并指定报告路径
pytest -v -s --html=reports/test_report.html tests/test_login.py
# 5. 失败重试(需安装pytest-rerunfailures:pip install pytest-rerunfailures)
pytest -v -s --reruns=2 tests/test_login.py # 失败后重试2次
2. 常见问题排查
(1)Fixture作用域冲突
问题:用例中调用的Fixture作用域不匹配(如类级Fixture被函数级用例调用)。
解决方案:统一Fixture作用域,优先使用function级作用域保证用例隔离;若需使用类级/模块级,确保所有调用的用例都适配。
(2)页面对象元素定位错误
问题:运行用例时提示"no such element",但元素定位表达式正确。
解决方案:检查是否需要显式等待(BasePage中已封装,确保调用了wait_element_clickable/wait_element_visible方法);检查元素是否在iframe中(需在BasePage中添加iframe切换方法)。
(3)测试报告无法生成
问题:执行生成报告命令后,未生成report.html文件。
解决方案:检查pytest-html是否安装成功(pip show pytest-html);确保命令中的报告路径正确(如reports目录需提前创建)。
(4)用例失败未截图
问题:用例失败后未生成截图。
解决方案:检查conftest.py中是否添加了pytest_runtest_makereport钩子函数(用于捕获用例执行结果);检查screenshots目录是否存在(代码中已添加自动创建逻辑)。
六、实战练习(巩固框架)
请基于上面搭建的框架,完成以下实战练习,巩固PO模式和Pytest的使用:
需求:新增"商品搜索"功能的测试用例,涵盖以下场景:
-
新增商品页面对象(product_page.py),封装商品搜索框、搜索按钮、搜索结果列表等元素和操作。
-
在tests目录下新增test_product.py,编写测试用例:
-
搜索存在的商品(如"Python编程书籍"),断言搜索结果正确。
-
搜索不存在的商品(如"xxx123456"),断言显示"无相关商品"提示。
-
搜索空值,断言显示"请输入搜索关键词"提示。
-
-
运行用例并生成测试报告,验证框架的复用性和可扩展性。
提示:新增页面对象时,继承BasePage类,复用公共操作;编写用例时,调用页面对象的方法,不直接操作元素。
七、总结与下一篇预告
本篇文章我们完成了企业级UI自动化框架的从0到1搭建,核心知识点总结:
-
框架的核心价值:解决零散脚本的冗余、维护难问题,实现可复用、易维护、可监控。
-
Pytest核心特性:Fixture(前置后置)、参数化(多组数据)、测试报告(直观展示)。
-
PO模式四层架构:配置层、工具层、页面对象层、测试用例层,职责清晰,分离解耦。
-
框架搭建关键:公共操作封装、元素定位集中管理、用例与页面分离。
下一篇文章我们将对框架进行进阶优化,学习数据驱动(Excel/JSON)、日志体系强化、失败重试等企业级特性,解决"多数据测试、问题定位难、脚本不稳定"三大核心痛点,让框架更贴合企业真实测试需求!
如果这篇文章对你有帮助,别忘了点赞+收藏+关注,后续会持续更新UI自动化系列教程~ 有任何问题欢迎在评论区留言!