☰
Python+Selenium+Pytest+Allure自动化测试框架从零搭建实践
2026/10/21 9:16:36 网站建设 项目流程

搭建Python + Selenium + Pytest + Allure自动化测试框架,从零开始的一次完整记录

最近和几个同行聊天,发现不少刚接触UI自动化测试的朋友都在纠结怎么搭一套能跑起来、能出报告、后续好维护的框架。工具链其实很明确,就是Python + Selenium + Pytest + Allure这四件套,但很多人在环境配置、目录结构、报告集成这三步上反复卡壳。我自己从最早用unittest写到后来切到pytest,再接入Allure,踩过不少坑。这篇文章就把我从零开始搭建这套框架的完整过程记录下来,包括每一步为什么这么选、具体怎么配、跑起来遇到哪些坑,希望能帮准备入坑的朋友省点时间。

1. 整体设计思路:为什么是这四个工具组合

先说选型。UI自动化测试的方案其实很多,Java系有TestNG + Selenium + ExtentReport,Python系也有Robot Framework这种关键字驱动框架。我最终选择Python + Selenium + Pytest + Allure,不是因为它最花哨,而是这套组合在灵活性、社区生态、学习成本之间拿捏得比较均衡。

Selenium是UI自动化的行业标准,WebDriver作为浏览器驱动的规范,几乎支持所有主流浏览器。Pytest相比unittest最大的优势是fixture机制和插件生态。fixture让你能优雅处理setup/teardown逻辑,比如登录、创建测试数据这些前置操作,不再像unittest那样要么塞setUp里、要么用一堆工具类来回传参。Pytest的断言直接用Python原生assert,写起来比TestNG那套断言库自然太多。Allure则是一份漂亮的测试报告方案,它的特性不止是显示用例通过失败,更重要的是能把测试步骤、附加信息、缺陷关联整合成可读性极强的可视化报告。

这三个工具的协作逻辑是这样的:Selenium负责操作浏览器,pytest负责组织和执行测试用例及管理测试生命周期,Allure负责收集执行结果并生成给人看的报告。Python作为胶水语言把它们串起来。

这套方案的另外一个关键优势是数据驱动和分层思想实现成本低。用一个Excel或YAML文件驱动上百条用例,在pytest + Fixture的参数化机制下是非常自然的事。测试代码、页面元素定位、测试数据分离,后续维护成本会低很多。如果你的团队里有测试开发能力一般但业务理解深的同事,这套框架的脚本层也非常容易上手。

2. 环境搭建与依赖安装

2.1 第一步:准备工作

开始动手前,先把基础环境理清楚。我用的是Windows 11开发机,Python版本3.10.9。官方其实对3.12以上版本支持也没问题,但为了避免个别库的兼容问题,建议优先选3.10或3.11这种社区验证比较充分的版本。

Python装好之后有一个地方必须注意:一定要用虚拟环境管理项目依赖。别图省事直接全局pip install,不然两个月后你再看自己机器上的包版本,会以为是被黑客攻击了。我通常用Python自带的venv模块,简单够用:

python -m venv venv

Windows下激活:

venv\Scripts\activate

macOS/Linux下激活:

source venv/bin/activate

我记得有一次在某台机器上忘了激活虚拟环境,直接把selenium装到了全局环境,后来项目里pytest版本和全局依赖互相打架,排查了整整一个下午。这个教训算是深刻。

2.2 安装核心依赖包

激活环境后,先升级pip,然后安装selenium和pytest:

python -m pip install --upgrade pip pip install selenium pytest

这里多说一句selenium版本的事。Selenium 4相比3.x有几个明显变化:一是内置了Selenium Manager,可以自动下载匹配浏览器版本的driver,省去了手动下载chromedriver的步骤;二是相对定位器的语法更简洁;三是等待API统一为WebDriverWait(driver, timeout)这种显式等待方式。如果你公司的网络环境允许,直接装最新的Selenium 4.x就好。

如果是老项目还在用Selenium 3,那么还需要单独管理webdriver的版本,手动下载驱动放到环境变量里。Selenium 4的Selenium Manager对本地开发来说省了很多事,但对测试环境或者CI环境,如果机器禁网,手动指定driver路径的方式反而更靠谱。这个我后面实战环节再细说。

继续安装Allure相关的pytest插件:

pip install pytest-allure-adaptor

等等,这个插件名字有点历史感。现在正确做法是安装allure-pytest,因为pytest-allure-adaptor早已停止维护,只支持pytest 3和Allure 1.x。如果你在网上搜到类似命令,注意甄别:

pip install allure-pytest

如果你想用Allure的behave模块,那就是allure-behave;pytest项目对应allure-pytest,别装混了。

2.3 Allure命令行工具的安装

Allure报告不仅要装pytest插件,还需要Allure命令行工具来把测试结果生成成最终的HTML报告。这一步很多新手会漏掉,结果运行完pytest发现怎么生成不了报告。

Allure命令行工具的安装方式有几种:

  1. Windows下如果装了Scoop,可以直接:
scoop install allure
  1. macOS下用Homebrew:
brew install allure
  1. 也可以去官方GitHub仓库下载zip包解压,然后把bin目录加到环境变量PATH里。

这里有个版本匹配的坑事:Allure命令行版本和allure-pytest插件版本需要兼容。一般建议都升级到当前稳定版本。allure-pytest 2.13.2配合Allure 2.24.1是我目前用得比较顺的组合。如果版本差距太大,可能出现测试执行正常但命令工具不识别结果文件的情况。

装好之后验证一下:

allure --version

能在终端打印出版本号,说明命令行工具OK了。

注意:如果你的开发机上有多个Python环境,要仔细确认pip install装到了哪个环境里。之前我遇到过在系统Python装插件、在虚拟环境执行pytest的情况,结果报告死活不生成。执行pip list确认allure-pytest已出现在当前虚拟环境里。

3. 项目结构设计

3.1 目录规划

搭建框架最忌一上来就写代码。我建议先花15分钟把项目目录想清楚,不然写到后期,代码文件散落一地,想找个测试数据都费劲。

我常用的目录结构是这样的:

project_root/ ├── config/ # 配置文件目录 │ ├── __init__.py │ ├── settings.py # 公共配置项 │ └── test_data.yaml # 测试数据文件 ├── common/ # 公共封装层 │ ├── __init__.py │ ├── base_page.py # 页面基类,封装Selenium常用方法 │ ├── logger.py # 日志封装 │ └── read_data.py # 读取测试数据的工具函数 ├── pages/ # 页面对象层 │ ├── __init__.py │ ├── login_page.py │ └── home_page.py ├── testcases/ # 测试用例层 │ ├── __init__.py │ ├── conftest.py # pytest fixture │ ├── pytest.ini # pytest配置 │ └── test_login.py ├── reports/ # 测试报告输出目录 ├── logs/ # 日志输出目录 ├── requirements.txt # 依赖清单 └── run.py # 测试入口脚本

这套结构遵循了分层测试的思想:页面对象层把元素的定位和页面的行为封装成方法,测试用例层只关心业务场景和断言,公共层封装所有可复用的基础能力,配置层把环境相关的硬编码参数全部抽离。

好处非常明显:页面UI变了,只需要对应修改page层的定位器;业务逻辑变了,改case层;环境地址变了,改配置文件。互不干扰。

3.2 conftest.py中fixture的设计思路

conftest.py是pytest中一个非常核心的机制,它可以让里面定义的fixture对所有测试用例可见,不需要每个测试文件都导入一遍。我通常在conftest里做这几件事:

定义driver的创建与销毁逻辑

import pytest from selenium import webdriver from selenium.webdriver.chrome.options import Options @pytest.fixture(scope="function") def driver(): options = Options() options.add_argument("--start-maximized") driver = webdriver.Chrome(options=options) driver.implicitly_wait(5) yield driver driver.quit()

scope="function"意味着每个测试函数都会创建一个干净的浏览器实例,测试之间互相隔离。如果你追求执行效率,可以改成scope="module"或scope="session",但那样用例间的状态影响是个隐患。我一般跑冒烟测试时用function,跑回归时用module级别并配合用例顺序控制。

处理失败截图

测试失败时自动截图是一个必备能力。在conftest里加一个钩子函数,每次用例结束后检查结果:

@pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): outcome = yield report = outcome.get_result() if report.when == "call" and report.failed: # 从fixture里拿到driver实例 driver = item.funcargs.get("driver") if driver: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") driver.save_screenshot(f"reports/screenshots/{item.name}_{timestamp}.png") # 附加到allure报告 with open(f"reports/screenshots/{item.name}_{timestamp}.png", "rb") as f: allure.attach(f.read(), name="失败截图", attachment_type=allure.attachment_type.PNG)

这个能力在实际工作中帮了我太多次。线上环境复现问题、开发不承认bug的时候,一张带时间戳的截图放在报告里,比什么都管用。

读取测试数据的fixture

比如我要把测试账号配置从yaml文件里读出来,就可以做一个简单的fixture:

@pytest.fixture def test_data(): return read_yaml("config/test_data.yaml")

这样每个case里直接调用test_data['username']即可,非常清爽。

3.3 pytest.ini配置

pytest.ini是pytest的主配置文件。这里有一个长期能困扰新人的点:网上教程里经常出现pytest.ini、pytest.cfg、pyproject.toml几种配置文件,到底该用哪个?我的建议很明确:用pytest.ini,这个文件名pytest优先级最高,不容易出问题。

一个基本配置模板:

[pytest] testpaths = testcases markers = smoke: 冒烟测试标记 regression: 回归测试标记

testpaths指定了pytest去哪里找测试用例。markers声明了自定义标记,避免pytest启动时出现warning。还可以加addopts来添加默认的参数:

addopts = -v -s --alluredir=./reports/allure-results --clean-alluredir

-v打印详细日志,-s让print输出可见,--alluredir指定allure结果文件的输出目录,--clean-alluredir每次跑之前清空上次的结果目录,非常实用。参数加得多,其实每个的命令行都可以先尝试,习惯了就会省时间。

4. 核心代码实现

4.1 浏览器驱动封装

前面conftest里直接用了webdriver.Chrome(options=options),实际项目中这样还不够。我建议在common/base_page.py里做一个封装,把所有Selenium常用操作一网打尽,后续写页面对象时可以少写大量重复代码。

from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By from selenium.common.exceptions import TimeoutException import logging class BasePage: def __init__(self, driver): self.driver = driver self.wait = WebDriverWait(driver, 10) def find_element(self, locator): try: return self.wait.until(EC.presence_of_element_located(locator)) except TimeoutException: logging.error(f"元素定位超时: {locator}") raise def click(self, locator): self.find_element(locator).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 def wait_element_visible(self, locator): return self.wait.until(EC.visibility_of_element_located(locator))

注意,我这里使用元组定位器,例如(By.ID, "username")。这是一种常见的约定,把元素如何定位的信息打包成一个元组传到封装方法里,好处是页面对象写的定位策略一目了然,便于统一维护。

为什么用显式等待而不是sleep?Selenium 4的底层逻辑里,显式等待通过轮询方式检查元素状态,一旦满足条件立即返回,大部分情况下把等待时间压得很低,浏览器空闲时休眠,效率比固定sleep高得多,也减少了因为网络慢导致的偶发失败。

4.2 页面对象模式的具体落地

页面对象模式是UI自动化里最经典的分层思想。原理非常朴素:每个页面都用一个类对应,页面上的每个可交互元素对应类中的一个方法,测试只跟方法打交道,不直接碰元素定位器。

登录页的页面对象:

from common.base_page import BasePage from selenium.webdriver.common.by import By class LoginPage(BasePage): username_input = (By.ID, "username") password_input = (By.ID, "password") login_button = (By.XPATH, "//button[@type='submit']") error_tip = (By.CLASS_NAME, "error-message") def login(self, username, password): 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_text(self.error_tip)

写到这里时我习惯把定位符全部放到类属性里,这样好处是元素换了选择器,只需要修改类属性一处。页面元素和逻辑分离,review代码的人也不用在一堆方法里翻定位符。

之前在某公司做过一个后台管理系统,改版时前端把按钮的dom结构换掉了,xpath路径全变。但因为我是把定位符集中在类属性里,只花20分钟把所有页面对象的定位符更新了一下,几十条测试用例全部恢复通过。如果当时图省事直接把xpath写死在方法里,那真就是个灾难现场了。

4.3 测试用例编写与数据驱动

写测试用例时pytest的语法有几个要点:测试函数必须以test_开头,pytest才能识别。断言直接用assert,失败的信息输出详细。下面是一个登录模块的实际用例:

import pytest import allure from pages.login_page import LoginPage @allure.story("用户登录") class TestLogin: @allure.title("正确的用户名和密码登录成功") @pytest.mark.smoke def test_login_success(self, driver): login = LoginPage(driver) login.login("correct_user", "correct_password") assert "首页" in login.get_page_title() @allure.title("错误的密码登录失败") def test_login_with_wrong_password(self, driver): login = LoginPage(driver) login.login("correct_user", "wrong_password") assert "用户名或密码错误" in login.get_error_message() @allure.title("用户名为空登录失败") @pytest.mark.parametrize("username,password", [ ("", "correct_password"), ("correct_user", ""), ("", "") ]) def test_login_empty_fields(self, driver, username, password): login = LoginPage(driver) login.login(username, password) assert "请输入用户名" in login.get_error_message() or \ "请输入密码" in login.get_error_message()

@pytest.mark.parametrize是pytest数据驱动的核心,上面的例子展示了同一个测试逻辑、多组测试数据怎么复用。parametrize的最大价值在于:每组数据都会被当作独立的测试用例执行,报告里能看到每一组的通过/失败状态,而不是只看到"循环执行了3次"。

如果想从外部文件读取测试数据,我习惯用yaml维护:

# config/test_data.yaml login_test_cases: case_001: username: "correct_user" password: "correct_password" expect: "登录成功" case_002: username: "" password: "correct_password" expect: "请输入用户名"

然后写一个工具函数读取,再配合parametrize使用。这种方式把测试数据和代码分离,业务同事也能维护数据文件。

4.4 Allure装饰器与报告生成

Allure报告之所以漂亮,很大程度上取决于你如何使用它的装饰器。常用的有:

  • @allure.epic("测试分组"):定义史诗级别,一般对应一块大的业务域
  • @allure.feature("功能模块"):定义功能模块
  • @allure.story("用户故事"):定义用户场景
  • @allure.title("用例标题"):自定义用例在报告里的显示名称
  • @allure.description("用例描述"):详细的文字说明
  • @allure.step("步骤"):标记关键操作步骤
  • @allure.attach(...):附加文件、文本、截图等内容

一个比较标准的用例写法:

@allure.epic("用户中心") @allure.feature("登录模块") @allure.story("密码错误处理") @allure.title("密码错误验证") @allure.description("验证输入错误密码时前端提示信息是否符合预期") def test_login_wrong_password(self, driver): with allure.step("打开登录页面"): login = LoginPage(driver) login.open() with allure.step("输入错误密码"): login.login("correct_user", "wrong_password") with allure.step("校验错误提示"): assert "用户名或密码错误" in login.get_error_message()

跑完测试之后,第一个命令行参数是生成allure测试原始结果,然后需要把原始结果渲染成静态HTML报告:

pytest testcases -v --alluredir=./reports/allure-results --clean-alluredir allure generate ./reports/allure-results -o ./reports/allure-report --clean allure open ./reports/allure-report

Windows下直接allure open会在默认浏览器打开报告页面,非常方便。

5. 执行管理与集成

5.1 如何灵活控制要执行的用例

项目大了之后,会有各种各样的测试执行场景:提交代码跑冒烟测试,每日全量回归,修改某个页面跑相关性用例。如果每次都用pytest命令行全量跑,效率太低。pytest的-m参数按标记过滤用例是按照需求非常顺手的操作:

# 只跑冒烟测试 pytest -m smoke # 只跑回归测试,不跑冒烟 pytest -m "regression and not smoke"

同时执行多个测试文件,可以用testpaths或在命令行直接指明:

pytest testcases/test_login.py testcases/test_order.py -v

如果用例名有规律,可以用-k模糊匹配:

pytest -k "login or user" -v

这些参数组合技建议提前在项目里跑一遍,后面配置CI会省心不少。

并行执行是另一个重要话题。几十上百条用例串行跑,耗费大量时间。pytest有pytest-xdist插件可以多进程并行执行:

pip install pytest-xdist pytest -n 4 # 4个进程并行

不过UI自动化并行有个隐蔽的问题:多个浏览器同时访问同一后台系统,并发操作同一个账号时可能互相影响(比如同时改一个配置项,后写的覆盖先写的)。所以并行前要评估测试环境的隔离能力。现在一些公司单我们内部会用不同的测试账号配合用例分组来解决这个问题。

5.2 与CI流程的对接思路

手工在命令行跑pytest当然也算能用,但是自动化测试的意义最终要落到持续集成上。通常的做法是在代码管理的流水线里加一个阶段:拉代码、装依赖、启动被测系统、跑测试、生成报告。

Allure的report在流水线系统里可以存放为一个无状态的静态文件,构建之后自己起内部服务投递和展示。也可以直接把allure-report目录挂到一个静态web服务下,团队内部通过浏览器访问。我在某公司就是这么干的:每次主线构建,流水线自动跑一遍回归测试,报告推送到内部的Allure Server,产品经理都能随时点开看那个版本的质量情况。

接入CI时有几个实操建议:

  • 用requirements.txt锁定依赖版本,避免环境差异导致测试结果漂移
  • CI机器上的浏览器和驱动要提前用脚本安装好
  • 设置合理的重试策略。UI测试本身的执行环境偶尔有网络波动,允许单个用例失败后自动重试1~2次,但重试结果要在报告中明确标识

重试插件推荐pytest-rerunfailures,在命令行加--reruns 2即可:

pip install pytest-rerunfailures pytest testcases --reruns 2 --reruns-delay 1

--reruns-delay是两次重试之间的间隔秒数。但是要注意,如果用例有写库操作,重试会重复执行写操作,可能导致数据异常。这类用例最好标记为不可重试,或者在用例内做好幂等处理。

5.3 Allure报告的阅读与维护

报告生成之后,日常维护里有一个经常被忽略的环节:定期查看报告的趋势。Allure报告自带趋势图,可以看到各轮测试结果的变化曲线。

第三种浏览习惯我也是后面带的组里才养成的:用例失败之后,第一时间看的不是堆栈,而是报告里这个用例的步骤记录、截图和日志。Allure steps能清晰地看到是哪一步操作开始失败的,往往比日志管用。这就意味着写用例时步骤标记要做全,不然排查问题只能靠猜。

另外报告里的分类(Categories)设置也是可以定制的。默认情况下Allure把失败原因分成缺陷类、崩溃类、测试失败类等。你可以根据自己的项目定制分类规则,比如把特定格式的报错归为"已知缺陷",这样报告第一眼就能知道哪几个失败属于已知问题,不用每次都重新确认。

6. 常见问题与排查技巧

6.1 高频问题速查

我在带新人的过程中,发现有不少问题几乎是每个初学这套框架的朋友都会遇到的,整理成一张速查表:

现象最常见原因解决方向
pytest运行时找不到测试用例testpaths配置指向了不存在的目录、测试文件没命名好检查pytest.ini的testpaths和test_开头文件命名规则
点击元素时一直超时元素在iframe里,或页面是异步加载先driver.switch_to.frame()切进去,再用显式等待
Allure报告生成为空allure-pytest插件没装对、结果目录未指定检查pip list、确认pytest --alluredir参数生效
用例之间互相干扰driver实例没有隔离,session级共享确认fixture的scope是function,或对session级做状态清理
并行执行时报资源冲突多个浏览器进程同时操作同数据隔离测试账号、分配互不干扰的数据集
弹窗定位不到是浏览器原生弹窗,不是页面元素用Alert类处理,或用user_data_dir预置浏览器配置
中文输入乱码输入法和send_keys冲突确保编码UTF-8,必要时清空后再输入,个别输入框可改用js赋值

表格只是把高频问题概括一下,实际上每个问题展开都有不少坑。拿iframe来详细说:现在很多系统前后端分离,页面里有大量嵌了iframe的子页面,比如富文本编辑器、地图组件。定位这类元素,常规的find_element肯定找不到,必须先切换上下文:

driver.switch_to.frame(driver.find_element(By.XPATH, "//iframe[@class='editor-iframe']")) # 操作iframe内的元素 element = driver.find_element(By.ID, "content") # 操作完切换回默认内容 driver.switch_to.default_content()

这段代码我写了很多遍,每次一看到"元素定位超时"第一反应就是检查是不是被iframe套住了。

6.2 环境相关疑难杂症

环境问题是最头疼的。有一次在某台CI机器上,明明装了Chrome和对应版本的driver,一跑就报unknown error: cannot find Chrome binary,排查了半天才发现Chrome安装路径跟Selenium默认查找路径不一致。解决办法一是设置环境变量,二是在option里显式声明二进制路径:

options.binary_location = "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"

这也是为什么我强调用配置管理浏览器路径的原因。

另一个高频问题是关于--headless模式。UI自动化在无显示环境下运行要加headless参数,但有些系统的前端对headless模式兼容性不好,可能出现脚本在本机有头模式跑得好好的,CI无头模式却各种诡异问题。建议在定位这类问题时,先在无头模式下手动访问一下被测系统主要页面,确认关键交互在headless下都正常。

还有一次遇到浏览器和driver版本不匹配的情况,报错信息又很绕。现在的Selenium Manager自动下载版本虽方便,但如果你公司网的网关钝化,还是手动指定driver版本最稳。我建议在项目初期就花点时间把driver放到独立目录,在配置文件声明版本路径,这样能规避掉很多环境问题。

6.3 框架维护的经验心得

这套框架跑起来大概用了半个多月就稳定了,但其正扎实是真运行小半年之后。迭代过程中不断有新页面、新逻辑加进来,维护工作主要有几块:维护页面对象的定位符,维护测试数据的一致性,以及新增用例遵循模型。这块没有银弹,关键是从一开始把分层做干净。

我个人的习惯是给每个页面对象加一段注释,注明页面入口URL、核心功能、最后修改时间,给每条用例注明关联需求点。代码本身就是最好的文档,这话比较老套但特别有用。

还有一点,定期清理失效用例。项目改版、功能下线都会让一些老用例长期失败。如果不及时处理,团队会慢慢对这些失败用例麻木,"反正那几条一直是红的,不用管"——这是自动化测试最危险的信号。一旦信任被破坏,整个框架的投入产出比就断崖式下跌。我现在基本保持每周扫一遍失败的用例,该更新定位符的当天就改,该删的直接删掉。

最后切个底层的经验。已经跑轮一次的用例能吃随机网络波动等环境因素影响,失败多次也可能命中产品缺陷,业务方确认功能正常的,直接归因到脚本脆弱性。想尽量消除不确定性,我有一套"三稳"原则:定位要稳(优先用id/data属性唯一标识)、等待要稳(显式等待覆盖90%场景)、数据要稳(用例不依赖上一条用例产生脏数据)。框架能稳稳跑起来,靠的就是把这三块做到位。

这套python+selenium+pytest+allure组合,我在不同的团队里搭过不少次,每次都能在一个星期内让新人写出第一版用例、出第一份报告。技术上真不是什么高深的东西,但把每一层的分工搞清晰、把环境问题彻底解决,后续的收益会非常可观。希望这篇文章能帮你少踩几个坑,一次把这个基础框架搭利索。后面遇到具体问题,欢迎随时交流讨论。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询