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 测试、视觉回归 低

先把手上的接口用例写稳、写薄,再考虑这些,顺序反了容易做成“看起来什么都有、实际什么都没守住”。

posted @ 2026-10-21 01:20  LauTianen  阅读(4)  评论(0)    收藏  举报