课程:B站大学
记录python学习,直到学会基本的爬虫,使用python搭建接口+UI自动化测试
用 Pytest + Requests + Allure +Playwright+Minium搭建接口自动化测试平台
-
- 为什么想写这篇文章
- 项目的情况
- 项目结构
- [HTTP 客户端封装](#HTTP 客户端封装)
- [conftest.py 全局配置](#conftest.py 全局配置)
- 环境配置
- [Fixture 封装](#Fixture 封装)
-
- [HTTP Client Fixture](#HTTP Client Fixture)
- [数据库 Fixture](#数据库 Fixture)
- [API 对象封装](#API 对象封装)
- 测试用例示例
-
- 接口测试用例
- [Web UI 测试用例(Playwright)](#Web UI 测试用例(Playwright))
- [小程序 UI 测试用例(Minium)](#小程序 UI 测试用例(Minium))
- 小程序页面对象封装
- [Allure 报告配置](#Allure 报告配置)
- 踩坑经验总结
-
- [1. Token 管理容易串场](#1. Token 管理容易串场)
- [2. 环境切换容易出错](#2. 环境切换容易出错)
- [3. 数据库连接容易超时](#3. 数据库连接容易超时)
- [4. 小程序自动化环境难搭](#4. 小程序自动化环境难搭)
- 常见问题
-
- [1. 导入报 ModuleNotFoundError](#1. 导入报 ModuleNotFoundError)
- [2. 数据库连不上](#2. 数据库连不上)
- [3. Token 为空](#3. Token 为空)
- [4. Playwright 浏览器没启动](#4. Playwright 浏览器没启动)
- 运行命令汇总
- 写在最后
- 实践是检验真理的唯一标准
在 AI 提效的时代,手工点 Postman 做回归简直是"反向进化"。
以前我也迷信 GUI,直到面对几百个接口时,点一下 Send 看一眼 Response,整个人都麻了。
后来我用 Pytest + Requests + Allure 重构了整套流程。
从此只有一个感受:早该这么干了。
为什么想写这篇文章
最近很多测试同行问我,你们公司的接口自动化是怎么做的?有没有什么经验可以分享?
说实话,我一开始也是从 Postman 过来的,踩了不少坑。网上关于 pytest 的教程很多,但大部分都是toy example,讲怎么写个简单的测试函数。真正告诉你怎么搭一个能跑起来的、覆盖三端(商户端、供应商端、配送端)的项目结构的教程,少之又少。
所以我想结合我维护的真实项目,手把手教大家怎么从零搭建一套接口自动化测试平台。
项目的情况
我这项目主要测的是一个电商系统的后端服务,包含:
- 商户端(B端):门店管理、会员系统、收银、库存
- 供应商端(S端):采购订单、售后处理、配送管理
- 配送端(Driver):配送小程序
测试内容包括:
- 接口自动化测试(主要用 requests)
- Web UI 自动化(用 Playwright 测 B 端后台)
- 小程序 UI 自动化(用 Minium 测微信小程序)
光说不练假把式,下面直接上代码。
项目结构
先看看整体目录结构:
pytest-tea-UI/
├── test/ # 测试用例放这儿
│ ├── apis/ # 接口封装
│ │ ├── merchant/ # B 端接口
│ │ └── supplier/ # S 端接口
│ ├── merchant/ # B 端用例
│ ├── supplier/ # S 端用例
│ ├── web_ui/ # Web UI 用例(Playwright)
│ └── mini_ui/ # 小程序用例(Minium)
├── fixtures/ # pytest 的 fixtures
├── config/ # 配置文件
│ └── environments/ # 多环境配置
├── mini_page_objects/ # 小程序页面对象
├── web_page_objects/ # Web 页面对象
└── conftest.py # pytest 全局配置
说实话,这个结构也不是一开始就想好的,是慢慢演进出来的。之前也踩过坑,比如一开始把测试用例和接口封装全放在一起,后来根本没法维护。
HTTP 客户端封装
这个是核心,我们把 requests 封装了一下,统一处理 Token、超时、重试。
python
# core/http/client.py
import requests
from requests.adapters import HTTPAdapter
from urllib3.util import Retry
from dataclasses import dataclass
from typing import Optional, Dict, Any
from config.settings import Config
@dataclass
class RequestContext:
"""请求的上下文配置"""
base_url: str = Config.BASE_URL
headers: Dict[str, str] = None
timeout: int = Config.TIMEOUT
def __post_init__(self):
if self.headers is None:
self.headers = dict(Config.HEADERS)
class HttpClient:
"""封装 requests,支持自动重试和 Token 注入"""
def __init__(self, context: Optional[RequestContext] = None):
self.context = context or RequestContext()
self.session = requests.Session()
self.session.headers.update(self.context.headers)
# 配置重试策略
retry = Retry(
total=3,
backoff_factor=0.3,
status_forcelist=[500, 502, 503, 504]
)
adapter = HTTPAdapter(max_retries=retry)
self.session.mount("http://", adapter)
self.session.mount("https://", adapter)
def request(self, method: str, endpoint: str, **kwargs):
url = f"{self.context.base_url}/{endpoint.lstrip('/')}"
response = self.session.request(method, url, **kwargs)
return response
def get(self, endpoint: str, **kwargs):
return self.request("GET", endpoint, **kwargs)
def post(self, endpoint: str, **kwargs):
return self.request("POST", endpoint, **kwargs)
这个封装干了几件事:
- 统一管理 base_url:不用每个请求都写完整的 URL
- 自动处理 headers:Token 什么的都放 Config 里
- 重试机制:遇到服务器抽风的情况,自动重试 3 次
- Session 复用:连接池,不用每次请求都新建连接
conftest.py 全局配置
这是 pytest 的入口文件,定义了一些全局的 fixtures 和配置。
python
# conftest.py
import sys
from pathlib import Path
# 把项目根目录加到 Python 路径
_ROOT_DIR = Path(__file__).resolve().parent
if str(_ROOT_DIR) not in sys.path:
sys.path.insert(0, str(_ROOT_DIR))
# 加载所有 fixtures
pytest_plugins = [
"fixtures.env",
"fixtures.db",
"fixtures.cache",
"fixtures.http",
"fixtures.identity",
"fixtures.browser",
"fixtures.mini",
]
VALID_ENVS = ["dev", "test", "prod"]
def pytest_addoption(parser):
"""自定义命令行参数"""
parser.addoption(
"--env", action="store", default="test", help="测试环境: dev/test/prod"
)
def pytest_collection_modifyitems(config, items):
"""自动检测是 S 端还是 B 端测试"""
from config.settings import Config
end_type = None
for item in items:
test_path = str(item.fspath).replace("\\", "/")
if "/driver/" in test_path:
end_type = "driver"
elif "/supplier/" in test_path:
end_type = "supplier"
elif "/merchant/" in test_path:
end_type = "merchant"
if end_type:
Config.CURRENT_END_TYPE = end_type
这个文件有几个要点:
- 路径配置:确保项目根目录在 Python 路径里,不然导入会报错
- 插件加载:fixtures 都在这儿声明,pytest 会自动加载
- 端类型自动检测:根据测试文件路径,自动识别是测 B 端还是 S 端,然后设置到 Config 里
环境配置
我们把环境配置抽离出来了,支持多环境切换。
python
# config/environments/test.py
BASE_URL = "https://test.example.com" # 替换为你们的测试环境地址
# B 端前端地址
MERCHANT_WEB_URL = "https://b-test.example.com"
# S 端前端地址
SUPPLIER_WEB_URL = "https://s-test.example.com"
# S 端账号密码(示例)
SUPPLIER_USERNAME = "test_user"
SUPPLIER_PASSWORD = "test_password"
# B 端手机号(示例)
MERCHANT_PHONE = "13800138000"
# 配送端 Token
DRIVER_TOKEN = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
切换环境很简单,直接加 --env 参数:
bash
pytest test/merchant/ --env=test # 测试环境
pytest test/merchant/ --env=prod # 生产环境(慎用)
Fixture 封装
Fixtures 是 pytest 的精髓所在,用好了可以大大减少重复代码。
HTTP Client Fixture
python
# fixtures/http.py
import pytest
from core.http.client import HttpClient, RequestContext
@pytest.fixture(scope="session")
def http_context():
return RequestContext()
@pytest.fixture(scope="session")
def http_client(http_context):
return HttpClient(context=http_context)
数据库 Fixture
python
# fixtures/db.py
import pymysql
import pytest
from config.mysql_conf import get_mysql_connection, DatabaseName
@pytest.fixture
def mysql_cursor():
"""提供数据库游标,用完自动关闭"""
def factory(database: DatabaseName):
conn = get_mysql_connection(database)
with conn.cursor() as cursor:
yield cursor
conn.close()
return factory
python
# config/mysql_conf.py
import pymysql
from enum import Enum
class DatabaseName(Enum):
MERCHANT = "merchant_db" # 商户数据库
SUPPLIER = "supplier_db" # 供应商数据库
def get_mysql_connection(database_name: DatabaseName):
return pymysql.connect(
host="your-mysql-host.example.com", # 替换为实际地址
port=3306,
user="your_db_user", # 替换为实际账号
password="your_db_password", # 替换为实际密码
database=database_name.value,
charset="utf8mb4",
cursorclass=pymysql.cursors.DictCursor
)
API 对象封装
封装接口调用,让测试用例更简洁。
python
# test/apis/merchant/merchant_oms_order_api.py
from test.apis.merchant.models import MerchantOmsOrder
from utils.request_handler import RequestHandler
class Merchant_Oms_OrderApi:
"""订单模块 API 封装"""
def __init__(self):
self.request_handler = RequestHandler()
def create_order(self, request_body: dict) -> dict:
"""创建商品订单"""
return self.request_handler.request(
"POST",
"/examp/order/create_order",
json=request_body,
return_type=RLong,
)
def get_order_detail(self, order_id: int) -> dict:
"""查询订单详情"""
return self.request_handler.request(
"GET",
f"/examp/order/{order_id}",
return_type=MerchantOmsOrderDetail,
)
def close_order(self, order_id: int) -> bool:
"""结账关单"""
return self.request_handler.request(
"POST",
"/examp/order/close_order",
json={"orderId": order_id},
return_type=RBoolean,
)
测试用例示例
接口测试用例
python
# test/merchant/ums/module/business/test_open_class.py
import allure
import pytest
from test.apis.merchant.merchant_shift_handover_record_api import (
Merchant_Shift_Handover_RecordApi,
)
@allure.epic("营业模块")
@allure.feature("交接班管理")
class TestOpenClass:
"""B端开班测试"""
@pytest.fixture(autouse=True)
def setup(self):
self.shift_handover_api = Merchant_Shift_Handover_RecordApi()
@allure.title("开班操作")
def test_open_class(self):
"""测试开班"""
# 开班操作
result = self.shift_handover_api.open_class(shop_id=1001)
assert result["code"] == 0
assert result["data"] is not None
@allure.title("关班操作")
def test_close_class(self):
"""测试关班"""
# 先开班
open_result = self.shift_handover_api.open_class(shop_id=1001)
# 再关班
close_result = self.shift_handover_api.close_class(
shop_id=1001,
handover_id=open_result["data"]["id"]
)
assert close_result["code"] == 0
Web UI 测试用例(Playwright)
python
# test/web_ui/merchant/test_simple_business_flow.py
import allure
import pytest
from playwright.sync_api import Page
from config.settings import Config
from web_page_objects.merchant.login_page import MerchantLoginPage
from web_page_objects.merchant.business.business_page import BusinessPage
@allure.epic("B端Web UI自动化")
@allure.feature("业务流程")
class TestSimpleBusinessFlow:
"""简单业务流程测试"""
@pytest.fixture(autouse=True)
def setup(self, page: Page):
self.page = page
self.login_page = MerchantLoginPage(page)
self.business_page = BusinessPage(page)
@allure.title("登录并进入营业页面")
def test_simple_flow(self):
"""测试登录并导航到营业页面"""
# 登录(手机号根据实际测试账号修改)
self.login_page.login(
phone="13800138000", # 替换为实际测试手机号
base_url=Config.MERCHANT_WEB_URL
)
# 进入营业页面
self.business_page.goto_business_page(Config.MERCHANT_WEB_URL)
# 验证
assert "business" in self.page.url.lower()
小程序 UI 测试用例(Minium)
python
# test/mini_ui/merchant/procurement/test_purchase_flow.py
import allure
import pytest
from mini_page_objects.merchant.purchase_page import PurchasePage
@allure.epic("B端小程序自动化")
@allure.feature("采购模块")
class TestPurchaseFlow:
"""采购模块自动化测试"""
@allure.title("从采购页进入商品详情")
def test_navigate_to_product_detail(self, mini_app):
"""测试:采购页 -> 点击商品 -> 商品详情页"""
purchase_page = PurchasePage(mini_app)
# 跳转到采购页
with allure.step("跳转采购页面"):
purchase_page.navigate_to_purchase()
mini_app.attach_screenshot("采购页面")
# 点击商品
with allure.step("点击商品"):
purchase_page.click_first_product()
# 验证进入详情页
with allure.step("验证进入详情页"):
current_page = mini_app.get_current_page()
assert "detail" in current_page.path.lower()
小程序页面对象封装
python
# mini_page_objects/merchant/procurement/purchase_page.py
from mini_page_objects.base.base_mini_page import BaseMiniPage
class PurchasePage(BaseMiniPage):
"""采购页面对象"""
PAGE_PATH = "/pag/chase/purchase"
SEARCH_INPUT = "van-search .van-search_field"
PRODUCT_ITEM = ".product-item, .goods-item"
ADD_TO_CART_ICON = ".van-icon-add-o"
def navigate_to_purchase(self):
"""跳转到采购页面"""
print(f"跳转到采购页: {self.PAGE_PATH}")
self.switch_tab(self.PAGE_PATH)
self.wait_for(lambda: self.page.path == self.PAGE_PATH, timeout=10000)
def click_first_product(self):
"""点击第一个商品"""
products = self.get_product_list()
if products:
products[0].click()
self.wait_for_timeout(2000)
def search_product(self, keyword: str):
"""搜索商品"""
self.fill(self.SEARCH_INPUT, keyword)
self.click(self.SEARCH_BUTTON)
Allure 报告配置
ini
# pytest.ini
[pytest]
addopts = --alluredir=reports/allure-results
testpaths = test
markers =
smoke: quick tests
regression: full regression tests
ui: Web UI automation
mini_ui: Mini app automation
运行测试并生成报告:
bash
# 运行测试
pytest test/merchant/ --env=test --alluredir=reports/allure-results
# 生成报告并打开
allure serve reports/allure-results
踩坑经验总结
1. Token 管理容易串场
B 端和 S 端的 Token 是不同的,一开始我们没做隔离,导致 Token 互串。后来在 Config 里加了 CURRENT_END_TYPE,根据测试文件路径自动切换。
2. 环境切换容易出错
一开始环境配置写在代码里,每次测完还要手动改。后来抽离到 environments/ 目录,通过 --env 参数控制。
3. 数据库连接容易超时
有些测试跑得比较久,MySQL 连接会超时。后来加了连接池管理,用完自动归还。
4. 小程序自动化环境难搭
Minium 需要微信开发者工具,而且不同端要用不同端口。一开始踩了不少坑,后来整理了一份排查文档。
常见问题
1. 导入报 ModuleNotFoundError
检查 sys.path 有没有包含项目根目录。conftest.py 里已经做了处理,但如果你直接跑单个文件可能有问题。
2. 数据库连不上
检查 MySQL 地址、端口、账号密码对不对。生产环境特别注意,别在生产环境跑测试!
3. Token 为空
检查 Config.CURRENT_END_TYPE 有没有正确设置。登录接口有没有返回 token。
4. Playwright 浏览器没启动
可能没安装浏览器:
bash
playwright install chromium
运行命令汇总
bash
# ============ 接口自动化 ============
# 运行 B 端测试
pytest test/merchant/ --env=test
# 运行 S 端测试
pytest test/supplier/ --env=test
# 只跑冒烟测试
pytest test/ -m smoke --env=test
# ============ Web UI 自动化 ============
# 有头模式(显示浏览器)
pytest test/web_ui/ --env=test --show-browser
# 无头模式
pytest test/web_ui/ --env=test
# ============ 小程序 UI 自动化 ============
pytest test/mini_ui/ --env=test
# ============ 生成报告 ============
pytest test/ --env=test --alluredir=reports/allure-results
allure serve reports/allure-results
写在最后
这套框架我也一直在维护使用,积累了不少经验。说不完美,但确实能干活。每次产品说"回归一下",我再也不用面对 Postman 一个个点了。
当然,这套东西也在不断演进。比如最近在考虑引入参数化测试、数据驱动、测试用例智能生成、引入智能体测试等。有兴趣的可以一起交流。
如果觉得有用,帮忙点个赞。有问题可以在评论区问,看到会回。

实践是检验真理的唯一标准
相关阅读:
- Pytest 官方文档:https://docs.pytest.org/
- Playwright Python 文档:https://playwright.dev/python/
- Minium 文档:https://minium.org/