☰
Python统一UI与接口自动化测试框架设计与实践
2026/10/28 8:45:46 网站建设 项目流程

简介:这是一套基于 Python 搭建的软件测试自动化框架,面向测试开发、自动化测试工程师,以及希望从手工测试向自动化转型的读者。框架以 unittest 为基础,采用页面对象模式与数据驱动思想,通过 Selenium 实现 Web UI 自动化,通过 Requests 实现接口自动化,并预留移动端扩展空间,既能支撑企业回归测试与持续集成,也适合个人系统学习自动化测试设计,同时充分考虑可维护性与复用性,让测试人员以较低上手成本编写用例,也方便开发团队统一维护公共方法。压缩包共 62 个文件,其中 25 个 Python 脚本承载核心逻辑,23 个 pyc 编译文件便于直接调用,另有 XML 工程配置、Excel 测试数据、文本依赖说明、ini 环境配置与 HTML 测试报告等辅助文件,整体仅 78KB,轻量紧凑、下载部署成本极低。资源自发布以来已有近 5000 人学习下载,目录按 pages、API、testcase、comm 等模块清晰划分,从页面对象封装、接口基础类、数据比较与加密处理,到日志记录、屏幕截图、邮件通知和测试报告生成均有完整示例,能够直观展示数据驱动与分层设计如何落地。框架还提供了第三方接口调用示例、配置读取与依赖管理,配套说明文档和运行清单,学习者可快速跑通流程,再按业务改造复用,最终拿到一套可直接扩展的自动化测试脚手架。

1. 软件测试自动化框架:UI和接口一体的Python方案

软件测试自动化框架这个话题,面试里被问烂了,但真到了项目里,大多数团队的情况是:Web UI 用 Selenium 写了几百条脚本,接口自动化拿 Postman 或 JMeter 顶一阵,两边各跑各的,报告不统一,配置不统一,CI 里维护两套工程。这个资源不是“又一份 Selenium 教程”,而是一个把 Web UI 自动化和接口自动化收进同一套 Python 框架的完整方案,核心栈是 Python + Selenium + unittest + requests:统一的分层结构、统一的配置入口、统一的日志和报告输出。它能直接回答一个问题——一条 UI 用例和一条接口用例,怎么在一个工程里用同一套规则组织、执行、留证据。适合已经会用 Selenium 写脚本、但想往“框架设计”走的测试开发,也适合要给团队从零搭自动化框架的测试组长:照着目录和基类抄,半天就能把第一版跑起来。

2. UI和接口共用一套框架:分层、基类与数据驱动

2.1 目录设计:为什么 UI 用例和接口用例不能分两套工程

很多项目里 UI 自动化和接口自动化是两个人各维护一套代码,环境地址一份写在 Selenium 脚本里,一份写在 requests 脚本里,改一个 IP 要动十几个文件。这套框架把两套用例放进同一个工程,靠目录分层把职责拆开,而不是靠物理隔离。

我一般会先把工程切成这样的结构:

auto_framework/ ├── config/ │ ├── global_config.py │ └── test_data/ │ ├── api_login.json │ └── api_user.json ├── common/ │ ├── base_case.py │ ├── browser_engine.py │ ├── http_client.py │ ├── log.py │ └── report.py ├── testcases/ │ ├── ui/ │ │ └── test_login.py │ └── api/ │ └── test_user_api.py ├── reports/ ├── logs/ └── requirements.txt

common 层放所有用例共用的东西:基类、浏览器驱动初始化、HTTP 客户端、日志封装、报告生成。testcases 下按 UI 和 API 分目录,但用例本身都继承同一个 common 基类。这样做的直接收益是:登录失败时,UI 用例和接口用例读的是同一份配置、打的是同一份日志,排查路径完全一致。

为什么不让 UI 和 API 各自一套工程?我会说一个非常现实的理由:CI 里你只需要跑一条命令python run.py,就能同时出两份报告,而不是先跑 UI 再单独拉接口的 Jenkins Job。耦合风险确实存在,但靠 common 层把共享代码收拢,比维护两套配置要省事得多。

2.2 BaseCase 封装:把 driver 初始化和日志收进基类

这个框架里最值得抄的就是BaseCase。它把 unittest 的 setUp/tearDown、driver 生命周期、日志获取全部收进去,用例文件里只写业务逻辑。

import unittest from selenium import webdriver class BaseCase(unittest.TestCase): """所有 UI / API 用例的公共基类""" driver = None @classmethod def setUpClass(cls): cls.driver = cls.get_driver() cls.logger = cls.get_logger() @classmethod def tearDownClass(cls): # 类级清理,避免每条用例都重启浏览器 if cls.driver: cls.driver.quit() def setUp(self): # 每条用例前重置隐式等待,防止用例间互相污染 if self.driver: self.driver.implicitly_wait(10) @staticmethod def get_driver(browser="chrome", headless=False): options = webdriver.ChromeOptions() if headless: options.add_argument("--headless=new") options.add_argument("--window-size=1920,1080") return webdriver.Chrome(options=options)

get_driver 用参数控制浏览器类型和无头模式,默认 chrome,日常调试可以不传 headless,CI 里再通过配置打开。setUp 里重置隐式等待是很多人会忽略的细节:如果某条用例把等待改成 30 秒又没还原,后面所有用例都被拖慢,甚至出现超时假失败。

注意这里故意没做 Page Object,因为这套框架面向中小规模项目,PO 模式适合页面多、复用度高的项目,硬上会把框架撑大。等用例超过两三百条再拆 PO 不迟。

2.3 HttpClient 封装:会话保持与 token 注入

接口用例最常见的失败不是断言错了,而是登录态丢了。用原生 requests 每一条用例都手动拼 header,一定会有人漏传 token。HttpClient用 Session 保持会话,构造时注入一次 token,后面所有请求自动带上。

import requests class HttpClient: """接口用例统一入口:会话保持、鉴权注入、统一超时""" def __init__(self, base_url, token=None, timeout=15): self.session = requests.Session() self.base_url = base_url self.timeout = timeout self.session.headers.update({"Content-Type": "application/json"}) if token: self.session.headers.update({"Authorization": f"Bearer {token}"}) def post(self, path, payload=None): resp = self.session.post( f"{self.base_url}{path}", json=payload, timeout=self.timeout ) return resp def get(self, path, params=None): resp = self.session.get( f"{self.base_url}{path}", params=params, timeout=self.timeout ) return resp

接口用例的基类可以单独继承 BaseCase,在 setUpClass 里初始化 HttpClient:

class ApiBaseCase(BaseCase): @classmethod def setUpClass(cls): cls.client = HttpClient(API_BASE, token=cls.get_token()) @staticmethod def get_token(): resp = requests.post(f"{API_BASE}/auth/login", json={ "username": LOGIN_USER, "password": LOGIN_PASS }) return resp.json()["token"]

token 只在类初始化时取一次,一个测试类里的所有接口用例共享,节省了每条用例重复调登录接口的开销。如果登录接口本身就慢,这个设计能让整套接口用例的执行时间从十分钟降到两分钟。

3. 从零跑通这套框架:环境准备、配置文件与第一条用例

3.1 requirements 锁版本:省掉一半的玄学报错

自动化框架跑不起来,十次里有六七次是依赖版本打架。Selenium 4 之后的 API 和 3.x 差别很大,requests 的老版本对一些新接口的响应解析也有坑。拿到这套资源后,第一件事是安装依赖,我建议直接用它带的 requirements.txt:

cd auto_framework pip install -r requirements.txt
selenium==4.15.0 requests==2.31.0 ddt==1.6.0 openpyxl==3.1.2 html-testrunner==1.2.1

锁版本的意义在 CI:今天装出来能跑,三个月后新成员pip install selenium装成 4.20,接口响应解析逻辑没变但底层行为变了,整个回归红一片。openpyxl 是留着做 Excel 数据驱动的,如果只用 JSON 数据源可以去掉。HTMLTestRunner 负责把 unittest 结果生成 HTML 报告,后面会用到。

3.2 全局配置:环境地址、超时和浏览器全部外置

配置是框架里最容易写出“硬编码灾难”的地方。这套资源把配置集中在config/global_config.py,而且支持环境变量覆盖,方便 Jenkins 里切换测试环境和预发环境。

import os class EnvConfig: # Web UI 被测系统地址 BASE_URL = os.getenv("BASE_URL", "http://192.168.1.10:8080") # 接口服务地址,和 UI 地址分离,避免前后端端口不同时改错地方 API_BASE = os.getenv("API_BASE", "http://192.168.1.10:8080/api/v1") BROWSER = os.getenv("BROWSER", "chrome") HEADLESS = os.getenv("HEADLESS", "0") == "1" UI_TIMEOUT = int(os.getenv("UI_TIMEOUT", "10")) LOGIN_USER = os.getenv("LOGIN_USER", "tester") LOGIN_PASS = os.getenv("LOGIN_PASS", "tester123")

推荐直接用环境变量覆盖,而不是在配置文件里写死多套环境用注释来回切。一个典型的用法是:

export BASE_URL=http://test-server:8080 export HEADLESS=1 python run.py

3.3 写第一条登录用例并跑起来

跑通一个最小闭环比把代码全部看懂更重要。先建一条最简单的 UI 用例:

from selenium.webdriver.common.by import By from common.base_case import BaseCase from config.global_config import EnvConfig class TestLogin(BaseCase): def test_login_success(self): self.driver.get(f"{EnvConfig.BASE_URL}/login") self.driver.find_element(By.ID, "username").send_keys(EnvConfig.LOGIN_USER) self.driver.find_element(By.ID, "password").send_keys(EnvConfig.LOGIN_PASS) self.driver.find_element(By.ID, "login_btn").click() # 登录成功后 URL 会进入 dashboard 页面 self.assertIn("dashboard", self.driver.current_url)
cd auto_framework python -m unittest testcases.ui.test_login -v

unittest 的用例发现机制有个硬规则:文件名必须test_*.py或*_test.py,类名继承TestCase,方法名必须以test_开头。三者缺一个,用例就会被静默跳过,控制台显示Ran 0 tests。这是新手最容易撞的墙,后面避坑章节还会细说。

-v参数会打印每条用例的执行结果,第一次跑建议加上。看到OK说明链路通了,接下来就可以把接口用例往里填。

4. 避坑与常见问题:从元素定位失败到报告乱码的五个现场

4.1 元素定位时不时失败:隐式等待把显式等待顶掉了

现象:同一条 UI 用例,本地跑十次能过八次,剩下两次在同一个输入框上报NoSuchElementException,重跑又好了。放到 CI 里失败率更高。

原因:BaseCase 在 setUp 里设置了implicitly_wait(10),而代码里如果又用了WebDriverWait的显式等待,两者会叠加成“等待时长翻倍”。更隐蔽的是,隐式等待对find_element生效,显式等待对同一个元素再等一次,两个机制互相干扰,某些情况下隐式等待反而让元素快照过期。

解决:一套用例里只用一个等待策略。我在这套框架里的习惯是——全局只留隐式等待兜底,页面跳转后用显式等待等关键元素,但不要在等待前后修改全局implicitly_wait的值。如果一定要混用,至少保证等待对象不同:隐式等待管页面加载,显式等待管元素可点击,别让它们盯同一个元素。

4.2 接口用例批量失败:登录态在用例间丢了

现象:test_login_success过了,但后面几十条业务接口用例全部返回 401。

原因:接口用例的 token 是在 ApiBaseCase 的setUpClass里获取的,这本身没问题。问题出在有人把获取 token 的代码写进了普通函数,每条用例执行时都重新登录一次,而测试环境的登录接口有并发限制,前一条用例的登录请求还没返回,后一条已经发出去了,服务端直接把后面的请求判定为重复登录,token 失效。

解决:token 只在类级别获取一次,并且用一个独立的get_token静态方法,不依赖测试用例执行顺序。更稳的做法是给get_token加一个模块级缓存变量,同一个测试进程只调一次登录接口。另外断言里不要只判断状态码是 200,很多接口业务失败时也返回 200,建议把响应体里的code字段一起校验。

4.3 生成的 HTML 报告乱码或打不开

现象:HTMLTestRunner 跑完,报告文件生成了,但浏览器打开全是乱码,或者显示空白。用记事本打开能看到中文字符变成æµÂ一类的怪串。

原因:HTMLTestRunner 默认用 ASCII 写文件,而测试用例的注释里全是中文,写入时编码失败,Python 3 下直接抛 UnicodeEncodeError,或者写出一个半截的 HTML。

解决:生成报告时用open(report_path, "wb")以二进制写入,并且报告内容统一转成 UTF-8。我习惯在 run.py 里把 output 编码固定住:

import codecs with codecs.open("reports/result.html", "w", encoding="utf-8") as f: runner = HTMLTestRunner( stream=f, title="自动化测试报告", description="UI + 接口全量回归" ) runner.run(suite)

这样报告的标题、用例名、错误信息里的中文都能正常显示。

4.4 执行器说 “Ran 0 tests”:用例被静默跳过

现象:命令行执行python run.py,日志显示Ran 0 tests in 0.000s,不报错也不跑用例。检查文件明明在 testcases 目录里。

原因:unittest 默认的 discover 只匹配test*.py文件,且只能从指定目录往下递归。如果 testcases 目录下没有__init__.py,或者用例文件命名成了login_test.py但类名没带 Test,discover 就找不到。还有一种情况:在 PyCharm 里右键单跑没问题,命令行一跑就 0 条,因为没有加-s testcases指定目录。

解决:执行命令固定写完整:

python -m unittest discover -s testcases -p "test_*.py" -v

用例文件一律test_xxx.py,测试类名一律TestXxx,目录下补上__init__.py。这三条缺一不可,缺任何一条都是 0 条用例的结局。

4.5 UI 和接口共用配置:环境切换时互相污染

现象:UI 用例和接口用例在本地都过了,切到 Jenkins 预发环境后,UI 通过了,接口却打到了测试环境的数据。

原因:UI 地址用BASE_URL,接口地址用API_BASE,我在前面的配置文件里把两者分开了。但有人写接口用例时图省事直接用BASE_URL拼接口路径。两套环境的前后端端口不一致时,接口请求就全打歪了。

解决:配置里强制区分BASE_URL和API_BASE,接口用例一律走API_BASE。在 CI 里给两个变量分别赋值,别用同一个地址。

5. 把框架推向日常:参数化、命令行执行与持续集成

5.1 用 DDT 把接口用例从重复代码变成数据

接口用例最大的痛点是相似用例太多:同一个查询接口,参数不同,断言不同,如果每个参数组合都写一个方法,接口一多文件就没法看了。这套框架里用 ddt 做数据驱动,把用例数据抽到 JSON 里。

import json import os import unittest import ddt from common.base_case import ApiBaseCase DATA_PATH = os.path.join(os.path.dirname(__file__), "..", "..", "config", "test_data", "api_login.json") @ddt.ddt class TestLoginApi(ApiBaseCase): @ddt.file_data(DATA_PATH) def test_login_cases(self, **case_data): username = case_data.get("username") password = case_data.get("password") expect_code = case_data.get("expect_code") resp = self.client.post("/auth/login", payload={ "username": username, "password": password }) self.assertEqual(expect_code, resp.json().get("code")) self.logger.info("login case: %s -> %s", username, expect_code)

对应的api_login.json:

{ "case_1": {"username": "tester", "password": "tester123", "expect_code": 0}, "case_2": {"username": "tester", "password": "wrong", "expect_code": 10001}, "case_3": {"username": "", "password": "", "expect_code": 10002} }

方法里**case_data会把 JSON 里的每个字段展开成关键字参数。以后增加用例只需要往 JSON 里加一条,不需要动代码。断言不再是死板的“状态码等于 200”,而是业务状态码等于接口定义的业务码,这样密码错误、参数缺失这类场景才能真正被覆盖到。

5.2 命令行全量执行与报告归档

日常开发时可以单跑某一个模块,但回归时必须全量执行。这套框架提供一个统一的入口run.py:

import unittest from common.report import generate_report if __name__ == "__main__": suite = unittest.defaultTestLoader.discover("testcases", pattern="test_*.py") generate_report(suite, "reports/regression_{}.html".format(time.strftime("%Y%m%d_%H%M%S")))

跑全量回归只需要一条命令:

cd auto_framework python run.py

报告文件带时间戳,历史报告不会被覆盖,想回溯某一次失败现场时,直接翻对应时间的 HTML 文件就行。日志和报告分两个目录:logs/放运行日志,reports/放测试报告,Jenkins 里只归档 reports。

5.3 Jenkins 流水线:从手动点脚本到定时回归

框架跑通之后最值得做的一件事是让它每天定时跑一次。Jenkins 配置里用一个 pipeline 脚本,把环境变量传进去,构建后归档报告即可。

pipeline { agent any stages { stage('Setup') { steps { sh "pip install -r requirements.txt" } } stage('Run Test') { steps { sh """ export BASE_URL=http://test-server:8080 export API_BASE=http://test-server:8080/api/v1 export HEADLESS=1 python run.py """ } } } post { always { archiveArtifacts artifacts: 'reports/*.html', allowEmptyArchive: true } } }

这里有一个容易被忽略的点:Jenkins 机器没有显示器,UI 用例必须开HEADLESS=1,否则 Selenium 会直接报cannot find Chrome binary或者 WebDriver 启动失败。第一次配 Jenkins 时先手动跑一遍HEADLESS=1 python run.py,确认无头模式没问题再上流水线。

6. 进阶验证:回归前我固定做这三项检查

6.1 失败截图:让每条失败用例自己留证据

UI 用例失败后,光看异常堆栈很难定位,尤其一些偶发问题,重跑就消失了。我给 BaseCase 的 tearDown 加了失败自动截图逻辑,每条失败用例都会在 reports 目录留下现场:

def tearDown(self): if self.driver and self._outcome.result.failures: shot_path = f"reports/fail_{self._testMethodName}.png" self.driver.save_screenshot(shot_path) self.logger.error("screenshot saved: %s", shot_path)

截图命名带用例名,和测试报告里的事件时间线对得上。从那以后我每次排查 UI 用例失败,都先看截图再看日志,而不是直接重跑赌运气。

6.2 接口用例执行前后清理数据

接口用例对测试数据的要求比 UI 更严格。新增、编辑、删除类用例跑完,数据会残留,第二次执行同样的用例就可能因为数据已存在而失败。我在框架里加了一个约定:每个接口测试类在 setUpClass 里准备数据,tearDownClass 里清理数据。清理动作直接调被测系统的删除接口即可,不连数据库,避免对测试环境造成不可控影响。

6.3 unittest 和 pytest 的迁移边界

最后说一个很多人纠结的点:这套框架用的是 unittest,而 pytest 现在更流行,功能也更丰富。我的建议是,如果是从零开始,可以直接用 pytest;但如果你的团队已经在用 unittest,没有必要为了“新潮”重构。unittest 的 discover 机制和 Jenkins 集成都非常稳定,ddt 做数据驱动完全够用。真有 pytest 的需求,等 pytest 生态的 fixture 能显著减少代码量时再迁移不迟,迁移成本主要在于 fixture 替换 setUp 和断言风格统一。

对比项unittest + ddtpytest
断言self.assertEqualassert
前置后置setUp / tearDownfixture
数据驱动ddt + JSON/Excelparametrize + yaml/Excel
失败重跑不内置,需自己写或第三方库内置 rerunfailures 插件
报告HTMLTestRunnerpytest-html / allure

这三点是我每次搭框架或接手新框架时强制走一遍的体检项目:失败能留证据、数据不残留、测试框架选型有边界。这套资源里的目录和基类可以直接套用,把第一版跑起来再慢慢加自己的业务用例,希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询