Python自动化测试笔记-pytest接口与UI
自动化不是“会用 pytest”就完事。一条用例从写出来到每天自动跑,中间隔着用例组织、请求封装、断言设计、报告呈现和接进 CI 好几段。之前这些笔记是分着记的:pytest 几篇、requests 几篇、UI 几篇。这篇按“一条用例的完整生命周期”重排,语法部分不在这里,默认已经会 Python。
一、先想清楚自动化测哪一层
测试金字塔放在自动化里更实用:越往下越快、越稳、维护成本越低,但离用户越远。
| 层级 | 写起来 | 跑起来 | 稳定性 | 建议占比 |
|---|---|---|---|---|
| 单元 | 开发写 | 毫秒级 | 高 | 大量 |
| 接口 | 测试写 | 秒级 | 较高 | 主力 |
| UI | 测试写 | 分钟级 | 低 | 只覆盖主流程 |
结论很直白:接口优先,UI 只兜核心链路。UI 用例写多了,页面一改就要跟着改,最后维护成本比手工还高。
有些场景不适合自动化:一次性活动、界面大改期、探索性测试、需要人判断视觉细节的地方。先判断值不值得写,再想怎么写。
二、pytest:一条用例长什么样
用例怎么被发现
| 层级 | 规则 |
|---|---|
| 文件 | test_*.py 或 *_test.py |
| 函数 | test_ 开头 |
| 类 | Test 开头,且不能带 __init__ |
| 方法 | Test 类里的 test_ 方法 |
断言就是一个 assert
def add(a, b):
return a + b
def test_add():
assert add(1, 10) == 11
失败时 pytest 会把两边的值都打出来,不用再写 assertEqual 那一套。
参数化:一个用例跑多组数据
import pytest
@pytest.mark.parametrize("x, y, expected", [
(1, 10, 11),
(1, 1, 2),
(1, 99, 100),
])
def test_add(x, y, expected):
assert x + y == expected
加 ids=["正常", "相等", "边界"] 可以让报告里的用例名可读,不然显示的是 test_add[1-10-11]。
标记与筛选
@pytest.mark.smoke
def test_login():
...
pytest -m smoke 只跑冒烟,-m "not slow" 排除慢用例。标记要先在 pytest.ini 里注册,否则一直报 PytestUnknownMarkWarning。
执行的常用参数
| 参数 | 作用 |
|---|---|
-v |
显示每条用例结果 |
-s |
允许 print 输出 |
-k 表达式 |
按名字筛选,支持 and / not |
-m 标记 |
按 marker 筛选 |
-x |
第一条失败就停 |
--maxfail=N |
失败 N 条后停 |
-n auto |
多进程并行(pytest-xdist) |
--reruns N |
失败重跑(pytest-rerunfailures) |
--html=report.html |
生成 HTML 报告(pytest-html) |
三、前置后置:从 setup 到 fixture
老式的四层钩子
setup_module > setup_class > setup_method > setup > 用例 > teardown > teardown_method > teardown_class > teardown_module
模块级只跑一次,类级每个类一次,方法级每条用例一次。它解决的是“顺序”问题,但解决不了“复用”:两个模块都要登录,就得写两遍。
fixture 解决复用
import pytest
import requests
@pytest.fixture(scope="session")
def api():
s = requests.Session()
s.headers.update({"User-Agent": "autotest/1.0"})
yield s
s.close()
yield 之前是前置,之后是后置。scope 决定这份资源活多久:
| scope | 生命周期 | 典型用途 |
|---|---|---|
function |
每条用例(默认) | 临时数据、页面对象 |
class |
每个类 | 一组用例共享的上下文 |
module |
每个文件 | 文件级的登录态 |
session |
整个会话 | HTTP 会话、数据库连接 |
conftest.py 放在目录里,该目录及子目录的用例直接就能用里面的 fixture,不用 import。加 autouse=True 可以自动生效,比如每条用例前清一次缓存。
fixture 之间还能互相依赖:def admin(api): ...,pytest 会先建 api 再建 admin,顺序不用自己管。
四、重跑、报告与日志
失败重跑
pytest --reruns 3 --reruns-delay 2
或者只给某条用例加 @pytest.mark.flaky(reruns=3, reruns_delay=2)。重跑只适合网络抖动、服务重启这类环境问题,真实的 bug 重跑一百次还是失败,别拿它掩盖问题。
报告
pytest-html 够用,看趋势和分组就上 allure:
import allure
@allure.feature("登录")
@allure.story("密码错误")
def test_login_wrong_pwd(api):
with allure.step("提交错误密码"):
r = api.post("/login", data={"user": "u", "pwd": "bad"})
assert r.json()["code"] != 0
失败时用 allure.attach 把请求体、响应体挂上去,报告里能直接看现场。pytest-allure-adaptor 已经废弃,装 allure-pytest,否则在 Python 3.8 以上会报 posonlyargs。
日志
用例里不要 print,用 logging 分级,跑完能按级别过滤:
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
)
五、接口:requests 怎么发对请求
一个请求的组成
import requests
r = requests.get(
"https://www.v2ex.com/api/nodes/show.json",
params={"name": "python"},
timeout=5,
)
print(r.status_code, r.json())
GET 用 params,表单 POST 用 data,JSON POST 用 json=。这三个别混:json= 会自动带 Content-Type: application/json,data= 走表单编码。
返回值看什么
| 属性 | 内容 |
|---|---|
status_code |
状态码 |
text |
解码后的字符串 |
content |
原始字节,图片、附件用这个 |
json() |
解析成 dict,格式不对会抛异常 |
headers |
响应头 |
elapsed |
耗时 |
中文乱码时用 r.content.decode("utf-8");不确定服务端编码就显式指定 r.encoding = "utf-8"。
保持登录态
s = requests.Session()
s.post(login_url, data={"user": "u", "pwd": "p"})
r = s.get(profile_url) # 自动带 cookie
登录接口返回 302 时,requests 默认跟跳转,导致中间那次响应的 Set-Cookie 拿不到。抓包确认后关掉跳转:
r = s.post(login_url, data=payload, allow_redirects=False)
location = r.headers["Location"]
豆瓣这类站点还会按 UA 拦脚本,默认 UA 直接 418,加上浏览器 UA 就正常。
超时与重试
默认没有超时,服务端卡住会把整条流水线拖死,每个请求都要带 timeout。不稳定的环境再加一层重试:
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
retry = Retry(total=3, backoff_factor=0.5, status_forcelist=[500, 502, 503, 504])
s.mount("https://", HTTPAdapter(max_retries=retry, pool_maxsize=20))
只对幂等请求重试,POST 重试可能造成重复下单。
六、接口用例怎么组织才不脆
分四层
project/
├── conftest.py # 会话、鉴权、日志
├── pytest.ini
├── config/ # 环境地址、账号
├── data/ # csv / yaml 测试数据
├── api/
│ ├── client.py # base_url、Session、统一异常
│ └── user.py # 一个接口一个函数
└── tests/ # 只写场景和断言
api/ 层负责拼 URL、带 token、处理通用错误;tests/ 层只表达业务意图。接口地址和账号放配置,不写死在用例里。
断言分三层
r = api.get_user(1001)
assert r.status_code == 200 # 传输层
assert r.json()["code"] == 0 # 业务层
assert r.json()["data"]["name"] == "张三" # 字段层
字段一多,逐层断言写不完,用 jsonschema 校验结构,再对关键字段做值断言:
from jsonschema import validate
validate(r.json(), {
"type": "object",
"required": ["code", "data"],
"properties": {"code": {"type": "integer"}},
})
失败信息也要能看:assert r.json()["name"] == "张三", r.text,把响应带上,省得再跑一次。
数据驱动
参数化配合 CSV,加一组数据只改文件:
import csv
import pytest
def load_cases():
with open("data/login.csv", encoding="utf-8") as f:
return list(csv.DictReader(f))
@pytest.mark.parametrize("case", load_cases(), ids=lambda c: c["title"])
def test_login(api, case):
r = api.post("/login", data=case)
assert r.json()["code"] == int(case["expect_code"])
用例之间不要有依赖
A 用例登录、B 用例下单,这种顺序依赖一旦并行就会挂。每条用例自己造数、自己清理,或者用 fixture 提供已登录的客户端。
七、UI:定位、等待、页面对象
App 和 Web 两条路
- App:uiautomator2,Python 在电脑上跑,
d(text="登录").click(); - Web:selenium,核心是定位加等待。
import uiautomator2 as u2
d = u2.connect()
d.app_start("com.example.app")
d(text="登录").click()
d(resourceId="com.example.app:id/et_phone").set_text("13800000000")
d.screenshot("shot.png")
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.ID, "submit"))
)
三种等待
| 类型 | 写法 | 特点 |
|---|---|---|
| 隐式等待 | driver.implicitly_wait(30) |
全局生效,找不到就轮询 |
| 显式等待 | WebDriverWait(...).until(...) |
等具体条件,最推荐 |
| 强制等待 | time.sleep(2) |
兜底,能不用就不用 |
uiautomator2 里对应的是 d.wait_timeout、d(text="x").wait(timeout=5) 和 sleep。
页面对象
元素和操作收进类,用例里只调方法,页面改版只改一处:
class LoginPage:
def __init__(self, driver):
self.driver = driver
def login(self, user, pwd):
self.driver.find_element("id", "user").send_keys(user)
self.driver.find_element("id", "pwd").send_keys(pwd)
self.driver.find_element("id", "submit").click()
失败现场
UI 失败光看 traceback 没用,至少留三样:截图、当前页面结构(page_source 或 dump_hierarchy())、当时的日志。放在 fixture 的 teardown 里,判断用例失败就自动存。
八、进 CI:让用例自己跑
Jenkins
构建步骤里“执行 shell”,然后加 HTML Publisher 归档报告,和之前那篇 Jenkins 集成 selenium 的配置是同一个套路:
cd $WORKSPACE
pip3 install -r requirements.txt
pytest -m smoke --html=report.html --self-contained-html
GitHub Actions
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- run: pip install -r requirements.txt
- run: pytest -m smoke --html=report.html --self-contained-html
- uses: actions/upload-artifact@v4
with:
name: report
path: report.html
Jenkins 里对应“归档构件”,GitHub 里对应 upload-artifact,作用一样:报告别只留在构建机上。
定时跑用 Jenkins 的轮询或 cron,GitHub 用 on.schedule。失败通知接企业微信、钉钉或邮件都行,关键是“失败要有人知道”,否则自动化跑挂了也没人看。
九、一个最小可跑的项目
project/
├── conftest.py # APIClient 与 session 级 fixture
├── pytest.ini # 路径、marker、默认参数
├── requirements.txt
└── tests/
└── test_nodes.py
conftest.py:
import pytest
import requests
class APIClient:
def __init__(self, base_url):
self.base_url = base_url.rstrip("/")
self.session = requests.Session()
self.session.headers.update({"User-Agent": "autotest/1.0"})
def get(self, path, **kwargs):
kwargs.setdefault("timeout", 5)
return self.session.get(self.base_url + path, **kwargs)
def post(self, path, **kwargs):
kwargs.setdefault("timeout", 5)
return self.session.post(self.base_url + path, **kwargs)
def close(self):
self.session.close()
@pytest.fixture(scope="session")
def api():
client = APIClient("https://www.v2ex.com")
yield client
client.close()
pytest.ini:
[pytest]
testpaths = tests
addopts = -v --strict-markers
markers =
smoke: 冒烟用例
tests/test_nodes.py:
import pytest
@pytest.mark.smoke
@pytest.mark.parametrize("name", ["python", "go"])
def test_node(api, name):
r = api.get("/api/nodes/show.json", params={"name": name})
assert r.status_code == 200
assert r.json()["name"] == name
跑:pytest -m smoke --html=report.html --self-contained-html。接口多了之后把 APIClient 和接口函数挪到 api/,conftest 只留 fixture,就是第六节那个结构。
十、维护与往下挖
自动化真正的成本在维护,不在第一版。几个经验:
- flaky 用例单独标记,每周看一次失败率,长期不稳的直接下掉或者改造成本更低的检查;
- 断言要能定位问题,失败信息带上请求和响应,别只留一个
assert False; - 数据要能造也要能清,依赖线上固定数据的用例迟早会挂;
- UI 用例控制数量,只覆盖登录、下单、支付这类主链路。
还想往上走的话,优先级大概是:
| 方向 | 补什么 | 价值 |
|---|---|---|
| 契约测试 | 用 schema 或 pact 校验接口约定 | 高 |
| 覆盖率 | pytest-cov 看用例漏了哪些分支 | 高 |
| 并行与设备池 | xdist、多机分配、资源隔离 | 中 |
| 性能 | locust / jmeter 接同一套接口封装 | 中 |
| 跨端 | Appium、Compose 测试、视觉回归 | 低 |
先把手上的接口用例写稳、写薄,再考虑这些,顺序反了容易做成“看起来什么都有、实际什么都没守住”。

浙公网安备 33010602011771号