抛弃 Postman!用 Pytest+Requests+Allure+Playwright+Minium 搭建高逼格接口+UI自动化测试平台

课程:B站大学
记录python学习,直到学会基本的爬虫,使用python搭建接口+UI自动化测试

用 Pytest + Requests + Allure +Playwright+Minium搭建接口自动化测试平台


在 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)

这个封装干了几件事:

  1. 统一管理 base_url:不用每个请求都写完整的 URL
  2. 自动处理 headers:Token 什么的都放 Config 里
  3. 重试机制:遇到服务器抽风的情况,自动重试 3 次
  4. 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

这个文件有几个要点:

  1. 路径配置:确保项目根目录在 Python 路径里,不然导入会报错
  2. 插件加载:fixtures 都在这儿声明,pytest 会自动加载
  3. 端类型自动检测:根据测试文件路径,自动识别是测 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 一个个点了。

当然,这套东西也在不断演进。比如最近在考虑引入参数化测试、数据驱动、测试用例智能生成、引入智能体测试等。有兴趣的可以一起交流。

如果觉得有用,帮忙点个赞。有问题可以在评论区问,看到会回。


实践是检验真理的唯一标准

相关阅读

相关推荐
木易GIS2 小时前
使用arcpy,批量读取多个文件夹的*.shp中的图层,统计提取图层的个数和要素总个数
python·arcgis
程序员小远2 小时前
Python+requests+unittest+excel 实现接口自动化测试框架
自动化测试·软件测试·python·测试工具·职场和发展·测试用例·excel
好家伙VCC2 小时前
# 发散创新:用Selenium实现自动化测试的智能断言与异常处理策略在现代Web应用开发中,*
java·前端·python·selenium
小陈工2 小时前
Python测试实战:单元测试、集成测试与性能测试全解析
大数据·网络·数据库·人工智能·python·单元测试·集成测试
2501_908329852 小时前
使用Python分析你的Spotify听歌数据
jvm·数据库·python
qq_416018722 小时前
使用Scikit-learn进行机器学习模型评估
jvm·数据库·python
m0_587958952 小时前
Python字典与集合:高效数据管理的艺术
jvm·数据库·python
2301_776508722 小时前
用Python制作一个文字冒险游戏
jvm·数据库·python
博士僧小星3 小时前
python3_scrapy_Requests类解析(请求与回应)
python·scrapy