简介:该自动化测试框架基于Python语言与unittest单元测试框架构建,面向软件测试工程师和自动化测试初学者,覆盖Web UI自动化与接口自动化两大应用场景。框架遵循PO设计模式,采用数据驱动思想,通过Selenium封装页面操作,借助Requests实现接口校验,并为移动端自动化预留扩展余地。压缩包共62个文件,整体仅78KB,以Python脚本为主体,辅以XML配置文件、TXT说明文档、Excel测试数据、HTML报告及依赖清单,结构清晰便于移植和二次开发。目前已有4942人学习下载,可结合配套博文快速掌握搭建思路。资源内含页面对象封装、基础API封装、数据比较、日志记录、邮件发送等模块,可直接复用或在此基础上扩展新的测试用例,显著降低自动化框架的搭建与维护成本。
1. 软件测试自动化框架到底在解决什么问题:从点按钮到跑流水线
接到一个迭代三年的订单系统,每次发版前要手工回归登录、下单、支付、对账的主流程,走一遍要一整天。这不是人力问题,是测试被重复劳动占住了。软件测试自动化框架的目标,是把 Web UI 上的点击和接口的请求都变成可重复执行的用例,让一条 pytest 命令代替一个晚上的通宵。
这个标题拆开看就是三件事。软件测试自动化框架指的是用例组织、配置管理、报告输出这一整套工程骨架;web ui 自动化处理浏览器侧的回归;接口自动化承担不依赖页面渲染的逻辑校验。两者复用同一套数据工厂、同一份报告、同一个 CI 任务,而不是各搭一套互不相干的脚本。
如果你是手工测试出身正准备搭第一套框架,或者现有脚本全是线性执行、换个环境就崩,按这个方案的思路走,两到三周能把第一条流水线跑起来。
2. 先把框架的骨干立起来:选型、分层与最小目录结构
2.1 语言与测试框架选型:pytest 为什么比 TestNG 和 unittest 更适合这套架构
选型这一步不决定上限,但决定团队能不能坚持维护。常见组合是 Python + pytest,理由不是语法难易,而是 pytest 的 fixture 机制与 requests、playwright 两个库的配合非常顺,接口和 UI 两条线都能在一个框架里被管理。
| 对比项 | pytest (Python) | TestNG (Java) | unittest (Python) |
|---|---|---|---|
| 断言风格 | 原生 assert | Assert.assertTrue 等 | assertEqual 等 |
| fixture 能力 | conftest + scope 作用域 | @BeforeClass / @DataProvider | setUp / tearDown |
| 数据驱动 | parametrize 直接可用 | DataProvider 需要配置 | 用 subTest 模拟 |
| 插件生态 | allure、rerunfailures、xdist 齐全 | 能接 Allure,但配置重 | 几乎没有 |
TestNG 适合团队已有 Java 微服务技术栈、接口定义都在 Java 侧的场景。unittest 只适合十几条用例的轻量脚本,用例多起来之后 setUp / tearDown 的组织方式很快就会捉襟见肘。
2.2 框架分层的三个部分:用例层、业务层、核心层
分层这件事,核心目标是隔离变化。我在这个标题的框架里做了三个层级。用例层(testcases)只描述业务操作和期望结果,比如“登录成功后 URL 跳转到首页”,用例里面看不到定位符和接口地址。业务层(pages 目录)把页面元素与操作收进页面对象,接口操作收进对应的 api 对象。核心层(core)放 requests 会话封装、浏览器工厂、断言工具和环境配置。
分层之后有个直接收益:页面改版时只需动 pages 目录,接口加字段时只需动 core 或对应业务对象,testcases 里的用例几乎不用落地改动。很多测试框架半年后烂掉,就是因为所有逻辑都堆在 testcases 里,互相牵连,一改全崩。
2.3 最小可用目录骨架:一个能直接落地的框架工程
这里给一个经过多个项目验证的目录结构,新建项目时我一般直接复制改名字。
auto_framework/ ├── config/ │ ├── __init__.py │ ├── settings.py # 环境地址、超时、重试次数的读取 │ └── environments.yaml # dev / staging / prod 的参数 ├── core/ │ ├── __init__.py │ ├── api_client.py # requests 会话封装 │ ├── browser_factory.py # 浏览器启动参数 │ └── assertion.py # 通用断言函数 ├── pages/ │ ├── __init__.py │ └── login_page.py # UI 页面对象 ├── testcases/ │ ├── __init__.py │ ├── test_login_ui.py # UI 用例 │ └── test_order_api.py # 接口用例 ├── data/ │ └── order_cases.yaml # 接口用例数据 ├── reports/ # 报告与截图输出 ├── requirements.txt ├── conftest.py # 全局 fixture └── pytest.ini # pytest 配置config 与 core 是整个框架的地基。config 不写死任何环境相关的值,所有地址从 environments.yaml 读;core 里不出现任何业务字段。pages 和 testcases 才是写业务的地方。这样安排之后,新成员接手的第一个任务通常是加一条用例,而不是去理解整个项目的调用关系。
2.4 配置与运行入口:pytest.ini、requirements.txt 和一条启动命令
pytest.ini 是这套框架的运行开关,所有全局行为都从这里声明。
[pytest] testpaths = testcases addopts = -q --alluredir=reports/allure-results pythonpath = . markers = ui: web ui 相关用例 api: 接口自动化用例testpaths 指定 pytest 去 testcases 目录收集用例;addopts 里把 Allure 报告目录固定好,后面就不用每次敲一长串参数。pythonpath = . 让项目根目录下的 core 和 pages 可以被直接 import,这一步很容易漏,漏了之后跑用例会报 ModuleNotFoundError。markers 用来区分 UI 和接口用例,跑的时候可以用 -m "ui" 或 -m "api" 精确圈定范围。
requirements.txt 是这套框架的依赖清单,按这个版本区间装基本不会踩版本坑。
pytest>=8.0 playwright>=1.45 requests>=2.32 pyyaml>=6.0 allure-pytest>=2.13 pytest-rerunfailures>=14.0环境地址通过环境变量 TEST_ENV 切换,conftest.py 里统一读取。
import os import pytest @pytest.fixture(scope="session") def base_url(): env = os.getenv("TEST_ENV", "staging") from core.settings import get_base_url return get_base_url(env)运行命令非常简单:
pytest testcases -m api --env=staging pytest testcases -m "not prod"这里 --env 只是演示一个常见的做法,conftest 里可以通过 pytest_addoption 把它变成命令行参数,也可以用环境变量直接透传。核心原则是测试代码里不出现写死的域名,所有环境信息只从配置层拿。
3. Web UI 自动化落地:用 Playwright 把浏览器操作做成可维护的用例
3.1 Playwright 带来的收益:自动等待让用例少一半玄学失败
做 web ui 自动化的人,大部分时间不是在写用例,而是在调等待。Selenium 时代最常见的是 WebDriverWait 加 expected_conditions,等元素可见、等按钮可点,代码里充满了各种 lambda。Playwright 内置了自动等待机制,fill、click 这些操作会自己等元素达到可操作状态,超时才报错。这直接砍掉了 UI 用例里最常见的一类不稳定因素。
另一个收益是上下文隔离。Playwright 的 browser.new_context() 可以创建独立的浏览器上下文,每个用例一个 context,cookie、localStorage 完全隔离,不需要像 Selenium 那样为了清状态反复启动浏览器。再加上 get_by_role 这种语义化定位,页面重构时用例的存活率明显更高。
3.2 在 conftest.py 里管理浏览器生命周期:browser 和 page 两个关键 fixture
浏览器对象适合做成 session 级,整个测试过程只启动一次;page 对象每个用例单独创建,保证用例之间互不污染。
# conftest.py(节选) import pytest from playwright.sync_api import sync_playwright @pytest.fixture(scope="session") def browser(): with sync_playwright() as p: browser = p.chromium.launch( headless=True, args=["--disable-gpu", "--no-sandbox"] ) yield browser browser.close() @pytest.fixture() def page(browser, base_url): context = browser.new_context( base_url=base_url, viewport={"width": 1920, "height": 1080}, locale="zh-CN", ) new_page = context.new_page() yield new_page context.close()headless=True 是 CI 环境的标配,本地调试时可以通过命令行加 --headed 跑有头模式,这比改代码快得多。--no-sandbox 是因为大多数 CI 容器以 root 用户运行,不加这个参数 Chromium 会直接报错退出。viewport 固定成 1920x1080 很重要,不然 CI 容器默认的 800x600 视口会让前端走响应式布局,很多元素的位置和可见性都变了,定位自然翻车。
base_url 是上一章定义的 fixture,它让 page.goto("/login") 这种相对路径写法成为可能,用例里不用拼完整的域名。context 里的 locale 参数固定为 zh-CN,这能保证页面上的中文文案和日期格式在 CI 和本地保持一致,避免因为语言环境不同造成断言失败。
3.3 把页面交互封装成业务操作:登录页面对象与用例写法
页面对象模式的核心是让用例读起来像业务文档。下面这个登录页把元素定位收进init,把操作收进方法。
# pages/login_page.py from playwright.sync_api import Page class LoginPage: def __init__(self, page: Page): self.page = page self.username = page.get_by_role("textbox", name="用户名") self.password = page.get_by_role("textbox", name="密码") self.login_btn = page.get_by_role("button", name="登 录") def goto(self): self.page.goto("/login") return self def login(self, username, password): self.username.fill(username) self.password.fill(password) self.login_btn.click() self.page.wait_for_load_state("networkidle") return self.page用例这一层就变得非常薄:
# testcases/test_login_ui.py import re import pytest from playwright.sync_api import expect from pages.login_page import LoginPage @pytest.mark.ui @pytest.mark.parametrize("username,password", [ ("tester01", "Passw0rd!"), ("tester02", "Passw0rd!"), ]) def test_login_success(page, username, password): LoginPage(page).goto().login(username, password) expect(page).to_have_url(re.compile(r"/home"))fill 和 click 自带自动等待,不需要在用例里写 sleep。expect 是 Playwright 的断言库,to_have_url 会持续轮询直到匹配或超时,默认超时时间是 5 秒,需要时可以单独传 timeout 参数。parametrize 把两组用户密码拆成两条独立用例,一旦有一组失败,另一组仍然能跑完并给出结果,这正是数据驱动该有的表现。
3.4 定位器策略与等待条件:UI 用例不稳定的主要来源
定位器选得好不好,决定用例在页面改版后还能不能活。我一般按这个优先级来:get_by_role 语义化角色定位,get_by_label 关联 label,get_by_placeholder 用占位符,id 兜底,最后才考虑 CSS 和 XPath。结构型 XPath 是最脆弱的,前端加一层 div,整条路径就断了。
# 推荐顺序 page.get_by_role("textbox", name="用户名") # 语义角色 page.get_by_label("密码") # label 关联 page.get_by_placeholder("请输入手机号") # 占位符 page.locator("#login-btn") # id 兜底 # 不推荐做首选 page.locator("//div[2]/form/div[1]/input") # 结构型 XPath等待条件上的踩坑主要集中在固定 sleep。wait_for_timeout 不是不能用,但它和真实条件无关,环境慢时超时、环境快时白等。正确的做法是依赖 Playwright 的自动等待,或者显式等一个业务信号。
# 错误:固定睡 3 秒 page.wait_for_timeout(3000) # 正确:等业务层面的结果 page.get_by_role("button", name="提交").click() page.wait_for_url("**/home") # 等 URL 变化 page.get_by_text("创建成功").wait_for() # 等业务文案出现备注:从 Selenium 迁移过来的同学最容易把 WebDriverWait 的习惯带进来,实际上 Playwright 的 action 内部已经有等待,自己再加 sleep 只是拖慢执行速度,并不会提高稳定性。
4. 接口自动化落地:pytest + requests 让接口用例进入同一套流水线
4.1 接口用例的形态:参数化与数据分离
接口自动化测试的起点不是代码,而是数据。把测试数据从用例里拎出来,放到 YAML 文件里,方便不懂代码的业务同学维护。
# data/order_cases.yaml - name: "正常订单查询" order_id: "SO20250101001" expected_status: 200 expected_code: 0 - name: "不存在的订单" order_id: "NOT_EXIST" expected_status: 404加载并参数化,用 pytest 的 parametrize 直接生成两条用例:
# testcases/test_order_api.py import yaml import pytest with open("data/order_cases.yaml", encoding="utf-8") as f: ORDER_CASES = yaml.safe_load(f) @pytest.mark.api @pytest.mark.parametrize("case", ORDER_CASES, ids=[c["name"] for c in ORDER_CASES]) def test_order_status(api_client, case): resp = api_client.get(f"/api/order/{case['order_id']}") assert resp.status_code == case["expected_status"]ids 参数让用例名在报告里显示成“正常订单查询”而不是 case0、case1,排错时一眼就能定位是哪条数据出了问题。YAML 文件用 utf-8 读取时必须指定 encoding,Windows 中文环境下不写这一行会报 UnicodeDecodeError,这是个非常容易翻车的细节。
4.2 统一请求封装:requests.Session、超时与鉴权处理
统一请求封装是为了解决三个实际问题:连接复用、统一的 header、统一的超时控制。直接用 requests.get 每次都是新连接,效率低且没法统一加鉴权头。
# core/api_client.py import requests class APIClient: def __init__(self, base_url: str, token: str = ""): self.session = requests.Session() self.base_url = base_url.rstrip("/") self.session.headers.update({ "Content-Type": "application/json", "User-Agent": "auto-framework/1.0", }) if token: self.session.headers["Authorization"] = f"Bearer {token}" def get(self, path: str, **kwargs): return self.session.get(self.base_url + path, timeout=15, **kwargs) def post(self, path: str, **kwargs): return self.session.post(self.base_url + path, timeout=15, **kwargs) def close(self): self.session.close()conftest 里对应暴露给测试用例的 fixture:
@pytest.fixture(scope="session") def api_client(base_url): token = os.getenv("AUTH_TOKEN", "") client = APIClient(base_url, token) yield client client.close()timeout=15 是必填项。requests 不写 timeout,网络不通时会一直挂在那里,CI 任务直接超时,整个流水线被一个接口拖死。token 从环境变量 AUTH_TOKEN 读取,不是写死在代码里,不然换环境或者 token 过期时得满仓库找。rtrim 处理 base_url 末尾的斜杠,避免和 path 拼接后出现双斜杠或者路径错乱。
4.3 断言怎么写才有效:状态码、业务码与响应体校验
接口断言的层级不止状态码一个。状态码只说明 HTTP 层通没通,业务码才说明业务到底成功没有。这两层都校验,才算断言到位。
# core/assertion.py import requests def assert_success(resp: requests.Response): assert resp.status_code == 200, \ f"HTTP 状态码 {resp.status_code}: {resp.text[:500]}" body = resp.json() assert body["code"] == 0, \ f"业务码 {body.get('code')}: {body.get('msg')}"在业务用例里的完整形态:
@pytest.mark.api def test_create_order_then_query(api_client): resp = api_client.post("/api/order", json={"product_id": "P1001", "amount": 2}) assert_success(resp) order_id = resp.json()["data"]["order_id"] resp = api_client.get(f"/api/order/{order_id}") assert_success(resp) assert resp.json()["data"]["status"] == "CREATED"断言里最忌讳的是断言失败后只报行号。上面栈里的 f-string 把实际响应体的前 500 字符打印出来,排查时直接看日志就能理解是服务端报错还是断言条件写错。对于复杂响应体,可以在 assertion.py 里再加 jsonschema 校验的封装,把接口返回结构和预期结构做整体比对,比逐字段 assert 更省事,也更稳定。
4.4 接口与 UI 共用一套数据工厂:让两条测试线协同
UI 用例常常需要“先有一个已创建的订单”,接口用例也需要“准备一个用户”。与其各自造数据,不如做一个数据工厂 fixture,通过接口创建业务数据,UI 用例和接口用例共用。
@pytest.fixture(scope="session") def create_order(api_client): def _create(product_id="P1001", amount=1): resp = api_client.post("/api/order", json={"product_id": product_id, "amount": amount}) assert_success(resp) return resp.json()["data"]["order_id"] return _createUI 用例拿到订单号后直接跳详情页验证渲染:
@pytest.mark.ui def test_order_detail_page_inherits_api_data(page, create_order): order_id = create_order("P1002", 3) page.goto(f"/order/detail/{order_id}") expect(page.get_by_role("heading", name=f"订单 {order_id}")).to_be_visible()这套做法消除了 UI 用例里最脏的环节:通过 UI 去创建前置数据。创建一个订单走完整个下单流程可能耗时两分钟,通过 API 创建只需要两秒。UI 用例专注于验证渲染和交互,数据准备交给接口层,这就是两条线共用数据工厂的核心收益。
5. 自动化框架最常见的五个翻车现场:定位失败、环境差异与数据耦合排查
5.1 开发机能跑,上了 CI 就挂:先把四处环境差异锁死
这是自动化框架落地时最打击士气的情况。本地执行全是绿灯,推到 CI 上跑,UI 用例一片红,接口用例也偶发失败。排查重点从来不是代码,而是环境差异。
常见原因和解法:
| 差异点 | 典型现象 | 解决办法 |
|---|---|---|
| 视口分辨率 | 元素被认为不可见、点击偏移 | conftest 里固定 viewport |
| 系统语言/字体 | 中文乱码、字体缺失导致布局变化 | Docker 镜像预装中文字体库 |
| 容器权限 | Chromium 启动报 sandbox 错误 | launch 参数加 --no-sandbox |
| 网络代理 | 内网 API 地址在 CI 不可达 | 环境地址配置化,通过 env 注入 |
建议本地调试用有头模式,CI 一律无头,并且把无头模式下的截图开启,这样失败时至少能看到现场。环境差异排查的常规路径是:先固定视口,再看依赖服务是否可达,最后检查字体与权限。
5.2 固定 sleep 越加越玄学:它是 UI 用例不稳定的头号根源
如果你发现脚本里 sleep 越来越长,用例还是隔三差五挂一次,大概率是把等待用错了。固定 sleep 和真实条件完全无关,环境快时白白浪费时间,环境慢时又等不够。之前调一个支付回调场景,sleep 从 2 秒加到 5 秒还是偶发失败,最后定位是回调通知存在 3 到 8 秒的 jitter,靠 sleep 永远等不对。
# 把这种 page.wait_for_timeout(5000) # 换成显式业务等待 page.get_by_text("支付成功").wait_for(timeout=10000)业务的确定性信号比时间推断可靠得多。Playwright 的操作自带自动等待,fill 和 click 会等元素可操作,真正需要手动等的是业务状态变化的时机,用 get_by_text、wait_for_url、wait_for_response 这类条件等待去卡点。
5.3 接口用例在 A 环境过、B 环境挂:配置和测试数据必须解耦
接口用例最隐蔽的坑是测试数据耦合环境。比如 order_id 在 staging 存在、prod 不存在,换环境跑就 404。原因往往是把写在 YAML 里的测试数据和环境绑死了,或者用例里直接用了别处查出来的 ID。
解决方式是让配置和数据分层。environments.yaml 只放每个环境独立的地址、账号、基础数据 ID;业务测试数据通过数据工厂动态生成,不落库到用例文件里。环境切换时只改配置不改用例,这才能保证一套用例多环境复用。
另外注意临时环境或测试环境的数据库经常被清空重启,昨晚还能跑的用例今早全挂,优先查数据是否存在,别急着改代码。
5.4 报告里只有失败堆栈没有上下文:缺截图和请求记录等于白跑
CI 上用例挂了,报告里只有一行 assert 堆栈。你根本不知道页面停在哪一步,接口返回的是什么。UI 用例需要失败截图和页面 URL,接口用例需要记录请求头和响应体。
UI 层接入 pytest hook,在用例失败时自动截图:
@pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): outcome = yield report = outcome.get_result() if report.when == "call" and report.failed: page = item.funcargs.get("page") if page: page.screenshot( path=f"reports/screenshots/{item.name}.png", full_page=True )这段代码的位置在 conftest.py,注意 reports/screenshots 目录要提前创建,否则截图保存会抛 FileNotFoundError。接口层可以在 api_client 的 session 上挂一个 HTTP 日志 adapter,把每次请求的 method、url、status code 打出来,失败时直接看日志定位。这层工作不复杂,但对排查效率的提升是决定性的。
5.5 用例串行全过、并行就挂:共享状态与资源冲突排查
pytest-xdist 加上之后,用例跑得快了,但偶发失败也来了。串行执行完全通过,一并行就出现创建订单返回重复数据、登录态互相覆盖之类的问题。原因基本是两类:全局共享了可变状态,或者测试数据存在唯一性冲突。
接口数据工厂里用时间戳拼 order_id 能解决唯一性碰撞;UI 用例用独立 context 解决登录态隔离。数据清理用 yield fixture 在 teardown 里删掉测试产生的数据,别依赖外部定时任务去清理。排查并行问题不要上来就怀疑 xdist 有 bug,先看有没有共享的模块级变量、共享的文件句柄、共享的数据库记录。
6. 把框架每天夜里自己跑起来:Allure 报告、失败重试与定时任务接入
6.1 Allure 报告的最小配置:从 pytest 结果到可读的 HTML
Allure 报告接入成本很低,requirements 里已经有了 allure-pytest,运行参数已在 pytest.ini 的 addopts 里声明了报告输出目录。本地执行完用 allure serve 起一个本地服务直接看结果。
pytest testcases -m api --env=staging allure serve reports/allure-results想在用例里补充业务描述和步骤,加两个装饰器就能在报告里看到结构化的行为描述,而不是裸的方法名。
import allure @allure.title("创建订单后查询状态流转") @allure.step("调用创建订单接口") def test_create_order_then_query(api_client): ...6.2 失败重试与定时任务:让框架进入无人值守状态
接口偶发超时和网络抖动是常态,可以在 pytest.ini 的 addopts 里加上重试策略:
addopts = -q --alluredir=reports/allure-results --reruns 2 --reruns-delay 3重试两次会让用例粒度不细的框架整体稳定性上一个台阶。注意:幂等性差的用例不适合重试,创建类接口重试前要确认是否会产生重复数据。
定时任务常见做法是 GitLab CI 的 schedule 触发,或者 Jenkins 的 cron。CI 任务里除了跑测试,还要把 reports/allure-results 作为 artifact 上传,这样每天醒来直接在流水线页面上看报告,不用登录服务器查文件。
我现在的习惯是接到新项目先花两天把这条流水线搭建起来,而不是等用例攒多了再重构。第一次看到夜里自动跑出来、带截图和完整调用链的全量报告时,前期踩的那些坑都值了。希望帮到你。
本文还有配套的精品资源,点击获取