Python + Pytest 接口自动化测试方案

本文涉及的产品
RDS MySQL DuckDB 分析主实例,集群系列 4核8GB
RDS DuckDB + QuickBI 企业套餐,8核32GB + QuickBI 专业版
RDS MySQL DuckDB 分析主实例,基础系列 4核8GB
简介: 本文介绍一套企业级Python+Pytest接口自动化测试框架,覆盖接口封装、YAML数据驱动、Allure可视化报告及Jenkins CI/CD集成,结构清晰、开箱即用,助力测试工程师高效落地自动化,支撑从小型项目到大型分布式系统的质量保障。

前言

Postman 仅适合单接口调试,面对批量回归、多环境验证和持续集成场景,手工操作效率极低且易出错。本文带你从零搭建一套企业级可落地的 Python + Pytest 接口自动化测试框架,覆盖接口封装、数据驱动、报告可视化、CI/CD 集成全流程,可直接用于小型项目,也可平滑扩展至大型分布式系统。

✅ 适合人群:

  • 接口测试工程师
  • 手工测试转自动化人员
  • 初级测试开发工程师
  • 需快速搭建自动化体系的团队

技术选型与优势对比

我们选择以下技术栈,兼顾易用性、扩展性和生态成熟度:

技术工具 核心作用 选型优势
Python 3.8+ 脚本开发语言 语法简洁、第三方库丰富、测试领域生态完善
Requests HTTP 请求发送 最流行的 HTTP 客户端,API 简单易用
Pytest 测试用例管理与执行 比 unittest 更灵活,支持参数化、fixture、插件扩展
Allure 测试报告生成 可视化效果好,支持用例分类、失败截图、历史趋势
YAML 测试数据与配置管理 可读性强,适合存储结构化数据
Jenkins 持续集成 开源免费,支持定时构建、代码触发、报告集成

标准化项目结构

采用分层设计思想,将配置、接口、用例、工具、数据分离,保证框架的可维护性:

api_auto_test/
├── config/                 # 环境配置目录
│   └── config.yaml         # 多环境配置(开发/测试/生产)
├── api/                    # 接口封装层(所有业务接口)
│   ├── __init__.py
│   └── login_api.py        # 登录接口封装
├── testcases/              # 测试用例层(仅写用例逻辑)
│   ├── __init__.py
│   └── test_login.py       # 登录模块测试用例
├── utils/                  # 工具层(通用方法)
│   ├── __init__.py
│   ├── request_util.py     # HTTP 请求封装
│   ├── assert_util.py      # 统一断言工具
│   └── log_util.py         # 日志工具(可选)
├── data/                   # 测试数据层(数据驱动)
│   └── login_data.yaml     # 登录模块测试数据
├── reports/                # 测试报告输出目录
├── requirements.txt        # 项目依赖清单
└── pytest.ini              # Pytest 全局配置文件

环境搭建与依赖安装

1. 基础环境要求

  • Python 3.8 及以上版本
  • pip 包管理工具

2. 安装依赖包

执行以下命令一键安装所有依赖:

pip install requests pytest pyyaml allure-pytest

3. 生成依赖清单

方便后续团队协作和 CI 集成:

pip freeze > requirements.txt

核心模块实现

5.1 多环境配置管理

config/config.yaml 中配置多环境信息,支持一键切换:

# 环境配置:dev-开发环境 test-测试环境 prod-生产环境
active_env: "test"

env:
  dev:
    base_url: "https://dev-api.example.com"
    timeout: 10
  test:
    base_url: "https://api.example.com"
    timeout: 10
  prod:
    base_url: "https://prod-api.example.com"
    timeout: 15

5.2 通用请求工具封装

utils/request_util.py 中封装 HTTP 请求,增加异常处理和日志打印:

import requests
import yaml
import logging
from typing import Dict, Any

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)

class RequestUtil:
    def __init__(self):
        # 加载配置文件
        with open("../config/config.yaml", "r", encoding="utf-8") as f:
            self.config = yaml.safe_load(f)
        # 获取当前激活的环境
        self.active_env = self.config["active_env"]
        self.base_url = self.config["env"][self.active_env]["base_url"]
        self.timeout = self.config["env"][self.active_env]["timeout"]

    def send_request(
        self,
        method: str,
        path: str,
        headers: Dict[str, str] = None,
        params: Dict[str, Any] = None,
        json: Dict[str, Any] = None,
        data: Any = None,
        **kwargs
    ) -> requests.Response:
        """
        统一发送 HTTP 请求
        :param method: 请求方法 GET/POST/PUT/DELETE
        :param path: 接口路径
        :param headers: 请求头
        :param params: URL 参数
        :param json: JSON 格式请求体
        :param data: 表单格式请求体
        :return: 响应对象
        """
        url = self.base_url + path
        logger.info(f"请求地址: {method} {url}")
        logger.info(f"请求参数: params={params}, json={json}")

        try:
            resp = requests.request(
                method=method,
                url=url,
                headers=headers,
                params=params,
                json=json,
                data=data,
                timeout=self.timeout,
                **kwargs
            )
            logger.info(f"响应状态码: {resp.status_code}")
            logger.info(f"响应内容: {resp.text[:500]}")  # 只打印前500字符,避免日志过长
            return resp
        except requests.exceptions.Timeout:
            logger.error(f"请求超时: {url}")
            raise
        except requests.exceptions.ConnectionError:
            logger.error(f"连接失败: {url}")
            raise
        except Exception as e:
            logger.error(f"请求异常: {str(e)}")
            raise

5.3 业务接口层封装

api/login_api.py 中封装登录接口,遵循一个接口一个方法的原则:

from utils.request_util import RequestUtil

class LoginApi:
    def __init__(self):
        self.request = RequestUtil()

    def login(self, username: str, password: str):
        """
        登录接口
        :param username: 用户名
        :param password: 密码
        :return: 响应对象
        """
        path = "/api/v1/login"
        payload = {
   
            "username": username,
            "password": password
        }
        return self.request.send_request(
            method="POST",
            path=path,
            json=payload
        )

5.4 数据驱动测试数据

data/login_data.yaml 中管理测试数据,实现数据与脚本分离

- case_name: 正常登录-正确用户名密码
  username: "test01"
  password: "123456"
  expect_status: 200
  expect_code: 0
  expect_msg: "登录成功"

- case_name: 登录失败-密码错误
  username: "test01"
  password: "wrong123"
  expect_status: 200
  expect_code: 1001
  expect_msg: "密码错误"

- case_name: 登录失败-用户名不存在
  username: "nonexist"
  password: "123456"
  expect_status: 200
  expect_code: 1002
  expect_msg: "用户不存在"

- case_name: 登录失败-用户名为空
  username: ""
  password: "123456"
  expect_status: 200
  expect_code: 1003
  expect_msg: "用户名不能为空"

5.5 测试用例编写

testcases/test_login.py 中编写测试用例,使用 Pytest 参数化实现数据驱动:

import pytest
import yaml
from api.login_api import LoginApi

# 加载测试数据
def load_login_data():
    with open("../data/login_data.yaml", "r", encoding="utf-8") as f:
        return yaml.safe_load(f)

class TestLogin:
    def setup_class(self):
        """测试类执行前的初始化操作"""
        self.login_api = LoginApi()

    @pytest.mark.parametrize("case", load_login_data(), ids=[case["case_name"] for case in load_login_data()])
    def test_login_cases(self, case):
        """登录接口测试用例"""
        # 发送请求
        resp = self.login_api.login(
            username=case["username"],
            password=case["password"]
        )
        # 断言
        assert resp.status_code == case["expect_status"]
        assert resp.json()["code"] == case["expect_code"]
        assert case["expect_msg"] in resp.json()["msg"]

5.6 统一断言工具

utils/assert_util.py 中封装常用断言方法,统一断言逻辑:

import requests
from typing import Any

def assert_status_code(resp: requests.Response, expect_code: int = 200):
    """断言响应状态码"""
    assert resp.status_code == expect_code, f"状态码错误,预期:{expect_code},实际:{resp.status_code}"

def assert_response_code(resp: requests.Response, expect_code: int = 0):
    """断言业务响应码"""
    assert resp.json()["code"] == expect_code, f"业务码错误,预期:{expect_code},实际:{resp.json()['code']}"

def assert_response_contains(resp: requests.Response, key: str, value: Any):
    """断言响应体包含指定键值对"""
    assert key in resp.json(), f"响应体中不存在键:{key}"
    assert resp.json()[key] == value, f"键值错误,预期:{value},实际:{resp.json()[key]}"

def assert_response_msg_contains(resp: requests.Response, expect_msg: str):
    """断言响应消息包含指定内容"""
    assert expect_msg in resp.json()["msg"], f"响应消息不包含:{expect_msg},实际:{resp.json()['msg']}"

测试执行与报告生成

1. 基础执行命令

在项目根目录执行以下命令运行所有测试用例:

pytest -v

2. 生成 Allure 测试报告

# 执行测试并生成 Allure 原始数据
pytest -v --alluredir=reports/allure-results

# 启动本地服务查看报告
allure serve reports/allure-results

# 生成静态 HTML 报告(可部署到服务器)
allure generate reports/allure-results -o reports/allure-report --clean

Pytest 全局配置

pytest.ini 中配置 Pytest 全局参数,简化执行命令:

[pytest]
# 默认命令行参数:-s 输出print信息 --tb=short 简化错误堆栈
addopts = -s --tb=short --alluredir=reports/allure-results
# 指定测试用例搜索路径
testpaths = testcases
# 指定测试文件命名规则
python_files = test_*.py
# 指定测试类命名规则
python_classes = Test*
# 指定测试方法命名规则
python_functions = test_*
# 标记用例(用于分组执行)
markers =
    smoke: 冒烟测试用例
    regression: 回归测试用例

Jenkins CI/CD 持续集成

将框架集成到 Jenkins,实现代码提交自动触发测试定时回归测试

  1. 安装插件:在 Jenkins 插件管理中安装 Allure PluginGit Plugin
  2. 新建自由风格项目
  3. 配置源码管理:填写 Git 仓库地址和分支
  4. 添加构建步骤:执行 Shell 命令
    # 安装依赖
    pip install -r requirements.txt
    # 执行测试
    pytest
    
  5. 配置构建后操作:添加 Allure Report,指定报告路径为 reports/allure-results
  6. 可选配置
    • 构建触发器:设置定时构建(如每天凌晨 2 点执行回归测试)
    • 构建通知:配置邮件通知,构建失败时发送邮件给相关人员

常见问题 FAQ

  1. Allure 安装失败怎么办?

    • Windows:下载 Allure 二进制包,解压后将 bin 目录添加到系统环境变量
    • Mac:执行 brew install allure
    • Linux:执行 sudo apt install allure
  2. 运行用例提示文件路径错误?

    • 确保在项目根目录执行 pytest 命令
    • 将相对路径改为绝对路径,或使用 os.path 模块动态获取路径
  3. 中文乱码问题?

    • 在打开文件时指定 encoding="utf-8"
    • 在 pytest.ini 中添加 env = LANG=zh_CN.UTF-8
  4. 如何处理需要 Token 的接口?

    • 将登录获取 Token 的操作封装为 Fixture,在需要的用例中引用
    • 将 Token 保存到全局变量或配置文件中,供后续接口使用

结语

接口自动化不是简单的“写脚本”,而是构建一套可持续维护、可扩展、可集成的质量保障体系。本文提供的方案是一个基础框架,你可以根据项目实际需求进行扩展,比如增加数据库操作、接口签名、文件上传下载等功能。

目录
相关文章
|
26天前
|
弹性计算 数据库 数据安全/隐私保护
SaaS系统技术实践,架构设计及应用场景
本文深入解析SaaS系统的技术实践(多租户隔离、微服务、自动化运维、安全合规)、分层架构设计(基础设施至前端五层)及典型应用场景(CRM、HRM、电商、政务、教育等),兼顾理论深度与落地可行性,助力构建高可用、可扩展、低成本的云原生SaaS系统。(239字)
211 7
|
25天前
|
数据采集 存储 API
阐述:淘宝 API 商品列表数据采集实战经验
本文分享淘宝商品列表API(taobao.items.search)合规采集实战经验,涵盖接口要点、签名加密避坑、限流应对及数据清洗技巧,强调“技术守规、艺术筛数、算术控本”,助力高效低成本获取高质量商品数据。(239字)
|
11天前
|
缓存 监控 前端开发
Shopee 商品数据高效抓取:请求与缓存校验 3 大核心
本文针对Shopee商品数据效率低、重复请求的痛点,提炼出经生产验证的3大核心方案:统一URL解析、精简关键请求头、ETag缓存校验,显著提升跨境选品、竞品分析与价格监控的稳定性与效率。
132 3
|
10天前
|
算法 索引
拍立淘 2026 技术拆解:多模态商品搜索的核心实现
拍立淘2026重构搜索底层逻辑:告别纯向量匹配,升级为“视觉理解+意图推理+决策生成”端到端系统。代码基于CLIP+Faiss实现多模态融合、混合召回与语义重排,支持价格/销量等意图驱动排序,工业落地需自研向量引擎与轻量意图模型。
57 1
|
13天前
|
数据采集 存储 监控
今日摸鱼不写代码 聊聊企业数字化底层基建
企业数字化常陷“重应用、轻基建”误区:盲目追求界面与功能,却忽视数据采集与同步这两大底层支柱。
117 2
|
19天前
|
存储 监控 API
基于Python的API调用与智能选品逻辑代码示例
本示例展示电商选品自动化逻辑:基于淘宝/1688等平台API,实现关键词搜索、销量/价格/起订量筛选、毛利率计算及差评关键词分析,输出高潜力商品(销量≥1000、差评率<5%、毛利率≥30%),含防封禁调用控制与可扩展架构。
129 3
|
18天前
|
数据采集 缓存 运维
1688商品详情API: 基于RESTful架构的商品数据接口请求机制与异步优化
本文以1688商品详情接口(item_get)为例,深入解析RESTful API的公共参数设计、同步调用缺陷及高并发下的异步优化策略,涵盖连接池、重试机制、协程并发与多级缓存等生产级实践,助力电商数据采集稳定高效。(239字)
123 1
|
1天前
|
数据采集 自然语言处理 数据可视化
京东商品评论接口技术实现:从接口分析到数据挖掘
本方案基于京东官方API,合规实现商品评论全链路分析:MD5+OAuth2双鉴权调用,180天数据采集;经清洗、SnowNLP情感分类(好评/中评/差评)与jieba-TFIDF关键词挖掘,生成情感分布饼图及差评词云,支持竞品调研与口碑监控。(239字)
15 0
|
1天前
|
API 数据处理 开发工具
API 与 SDK 三大核心区别
API是定义交互规则的接口契约,无具体实现;SDK则是封装了API、逻辑代码与工具的开箱即用开发包。API需手动处理请求/解析/异常,成本高;SDK仅需导入调用,极简高效。SDK必然包含API,但API不依赖SDK。
9 0
|
1天前
|
Java 调度
并发接口调用稳定性优化:特征隔离与流量整形实操
线上调用第三方接口易被限流,主因是请求特征高度规整。本文提供两套实战方案:一是**线程特征隔离**——每线程独享随机UA、Headers、Session及初始化延迟;二是**自适应流量整形**——动态冷却、抖动退避、失败重试降频。代码简洁,开箱即用。(239字)
11 0