Python接口自动化测试框架搭建:pytest+requests实战

阿麦
阿麦
发布时间:2026-11-12 | 3人浏览过
原创

基于Python生态中的pytest测试框架与requests库,从工程结构设计、Session统一会话封装、YAML数据驱动、Fixture鉴权管理到测试报告生成的完整接口自动化测试框架实战教程。

技术实战 技术栈:Python / pytest / requests / PyYAML 适用场景:RESTful 接口自动化回归测试
Python接口自动化测试框架搭建:pytest+requests实战

搭建一套高可用接口自动化测试框架的核心目标,是将零散的手工接口调用转化为具有高复用性、可自动回归且能够生成结构化报告的工程化系统。本文基于 Python 生态中轻量且强大的 requests 库与企业级测试框架 pytest,从工程目录骨架划分、requests.Session 统一会话封装、环境多配置隔离、YAML 数据驱动设计到前置 Fixture 鉴权共享,一步一步搭建一套结构分明、易于维护的接口自动化测试框架。

一、框架设计思想与分层架构

在初期的接口测试中,最容易出现的坏味道是将网络请求、用例逻辑、测试数据以及断言硬编码在同一个测试函数里。当接口地址发生变动或业务字段微调时,需要逐行翻找修改每个测试文件,维护成本极高。一套合格的测试框架应具备“数据、逻辑与底层调用彻底分离”的特征。

在本项目中,我们将整体框架划分为四个标准层级:基础配置与数据层、固件与环境调度层、网络请求客户端层、用例编写与多级断言层。各层之间保持单向依赖,使得上层用例无需关注底层的网络会话建立和鉴权细节。

Python接口自动化测试框架搭建:pytest+requests实战
图 1:pytest + requests 接口自动化测试框架四层架构设计

各分层在工程实践中的职责如下:

  • 基础配置与数据驱动层(configs / data):负责统筹管理多套运行环境的域名与全局参数,并将用例参数结构化保存在 YAML 文件中,保证用例代码与动态数据解耦。
  • 固件与环境调度层(conftest.py / fixtures):依托 pytest 独有的 fixture 机制,在用例运行前统一完成测试用户登录、提取并在内存中缓存 Token,在测试结束时执行脏数据还原。
  • 请求客户端层(apis):基于 requests.Session 封装统一的 BaseClient,负责 URL 拼接、全局请求头拦截、网络异常捕获以及统一的请求与响应耗时日志记录。
  • 用例与断言层(cases):测试方法专注业务流转,通过参数化批量注入测试数据,并依据统一标准实施 HTTP 状态码、业务返回码及核心实体字段的三重断言。

二、工程结构设计与环境依赖准备

一个整洁规范的工程目录是测试框架平稳维护的基石。在项目根目录下,按照功能职责将配置文件、测试数据、接口封装类、测试用例和公共工具类分开存放。

api_test_framework/
├── apis/                   # 接口对象层:按业务模块封装接口调用
│   ├── __init__.py
│   ├── base_client.py     # 核心请求客户端(基于 requests.Session)
│   └── user_api.py        # 用户模块具体接口定义
├── cases/                  # 测试用例层:pytest 测试脚本
│   ├── __init__.py
│   └── test_user.py       # 用户模块测试用例
├── configs/                # 配置目录:多环境配置文件
│   └── config.yaml        # 基础服务配置(域名、超时、默认账号)
├── data/                   # 数据驱动目录:存放 YAML 格式的测试用例数据
│   └── user_cases.yaml    # 用户模块参数化数据
├── reports/                # 运行产物:存放测试报告与运行日志
├── utils/                  # 工具类:配置文件读取、日志记录等
│   ├── __init__.py
│   └── yaml_handler.py    # YAML 文件读取解析工具
├── conftest.py             # pytest 全局共享 fixture(鉴权、客户端实例初始化)
├── pytest.ini              # pytest 核心执行参数配置
└── requirements.txt        # 项目依赖清单

创建 Python 虚拟环境并安装核心依赖包。本项目所需的库包括处理 HTTP 请求的 requests、测试运行引擎 pytest、生成图形化测试报告的 pytest-html 以及解析数据配置文件的 PyYAML:

# 依赖安装示例命令
pip install requests==2.31.0 pytest==8.1.1 pytest-html==4.1.1 PyYAML==6.0.1
Python接口自动化测试框架搭建:pytest+requests实战
图 2:框架搭建与测试执行完整链路流程图

在根目录创建 pytest.ini 文件,规范 pytest 的默认运行行为。通过 addopts 指定报告输出路径,通过 testpaths 指定默认搜索用例的目录:

[pytest]
# 默认追加命令行参数:生成独立自包含的 HTML 测试报告,并输出简要执行日志
addopts = -vs --html=reports/report.html --self-contained-html
# 默认用例搜索路径
testpaths = cases
# 匹配测试文件规则
python_files = test_*.py
# 匹配测试类规则
python_classes = Test*
# 匹配测试函数规则
python_functions = test_*

三、配置管理与多环境切换实现

在实际研发过程中,接口通常需要在开发环境(dev)、测试环境(test)与预发布环境(staging)中频繁切换。绝不能在代码中将 http://api.test.local 硬编码。通过 config.yaml 集中配置,再通过 Python 读取并支持动态切换。

在 configs/config.yaml 中定义环境基地址及通用设置:

# configs/config.yaml
env: "test"  # 当前激活环境

environments:
  dev:
    base_url: "https://dev-api.example.com"
    timeout: 10
  test:
    base_url: "https://test-api.example.com"
    timeout: 15
  staging:
    base_url: "https://staging-api.example.com"
    timeout: 20

default_user:
  username: "test_engineer"
  password: "SafePassword123!"

在 utils/yaml_handler.py 中封装针对 YAML 文件的安全读取函数,保证文件路径基于项目根目录自动解析,避免在不同工作目录下执行时找不到配置文件:

# utils/yaml_handler.py
import os
import yaml

# 获取项目根目录绝对路径
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

def load_yaml(relative_path: str) -> dict:
    """安全读取项目内的 YAML 数据文件"""
    file_path = os.path.join(ROOT_DIR, relative_path)
    if not os.path.exists(file_path):
        raise FileNotFoundError(f"配置文件未找到: {file_path}")
    with open(file_path, "r", encoding="utf-8") as f:
        return yaml.safe_load(f) or {}

def get_config() -> dict:
    """获取当前生效环境的配置信息"""
    all_config = load_yaml("configs/config.yaml")
    active_env = all_config.get("env", "test")
    env_config = all_config.get("environments", {}).get(active_env, {})
    env_config["default_user"] = all_config.get("default_user", {})
    return env_config

四、核心请求客户端封装(requests.Session)

直接在每个用例中使用 requests.get() 或 requests.post() 会带来三大痛点:无法自动复用底层 TCP 保持连接池、无法统一拦截注入 Authorization 鉴权头、网络超时与报错无法统一记录。

为此,我们设计一个基类 BaseClient,内部持有 requests.Session 实例,封装所有标准 HTTP 方法,并提供统一的异常兜底与调试日志打印。

# apis/base_client.py
import logging
from urllib.parse import urljoin
import requests

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")

class BaseClient:
    def __init__(self, base_url: str, timeout: int = 15):
        self.base_url = base_url.rstrip("/") + "/"
        self.timeout = timeout
        self.session = requests.Session()
        # 默认通用请求头
        self.session.headers.update({
            "Content-Type": "application/json",
            "Accept": "application/json"
        })

    def set_auth_token(self, token: str):
        """为当前会话设置统一的鉴权 Bearer Token"""
        self.session.headers["Authorization"] = f"Bearer {token}"

    def request(self, method: str, url: str, **kwargs) -> requests.Response:
        """核心统一请求方法:处理路径拼接、超时设置与日志记录"""
        full_url = urljoin(self.base_url, url.lstrip("/"))
        kwargs.setdefault("timeout", self.timeout)

        logging.info(f"发起请求: [{method.upper()}] {full_url} | 参数: {kwargs.get('json') or kwargs.get('params')}")
        
        try:
            response = self.session.request(method=method, url=full_url, **kwargs)
            logging.info(f"响应结果: 状态码 [{response.status_code}] | 耗时: {response.elapsed.total_seconds()}s")
            return response
        except requests.exceptions.Timeout:
            logging.error(f"请求超时: [{method.upper()}] {full_url} 超过 {self.timeout} 秒")
            raise
        except requests.exceptions.RequestException as e:
            logging.error(f"网络请求异常: {e}")
            raise

    def get(self, url: str, **kwargs) -> requests.Response:
        return self.request("GET", url, **kwargs)

    def post(self, url: str, **kwargs) -> requests.Response:
        return self.request("POST", url, **kwargs)

    def put(self, url: str, **kwargs) -> requests.Response:
        return self.request("PUT", url, **kwargs)

    def delete(self, url: str, **kwargs) -> requests.Response:
        return self.request("DELETE", url, **kwargs)

在具体业务中,继承 BaseClient 编写业务接口类。例如用户中心模块 UserApi:

# apis/user_api.py
from apis.base_client import BaseClient
import requests

class UserApi(BaseClient):
    """用户中心模块相关 API 封装"""

    def login(self, username: str, password: str) -> requests.Response:
        """用户密码登录接口"""
        payload = {"username": username, "password": password}
        return self.post("/api/v1/auth/login", json=payload)

    def get_profile(self) -> requests.Response:
        """获取当前登录用户个人资料"""
        return self.get("/api/v1/user/profile")

    def update_profile(self, nickname: str, email: str) -> requests.Response:
        """更新个人资料接口"""
        payload = {"nickname": nickname, "email": email}
        return self.put("/api/v1/user/profile", json=payload)

五、Fixture 鉴权管理与数据流转拓扑

在接口自动化测试中,绝大部分业务接口都需要携带登录态凭证(如 JWT Token)。若在每一个测试用例前都调用一次登录接口,不仅会导致测试整体运行耗时成倍增加,还会因为测试并发请求触发网关登录频次限流。

pytest 提供了强大的 Fixture 机制,支持设置 scope="session"。这使得在整个测试执行过程中,仅在启动初期执行一次用户登录,提取返回的 Token 并直接注入到 UserApi 的全局会话头中,后续所有用例直接共享已鉴权的客户端对象。

Python接口自动化测试框架搭建:pytest+requests实战
图 3:接口调用、Session 会话与测试数据流转拓扑

在根目录下编写 conftest.py,提供可供所有用例直接注入的 fixture:

# conftest.py
import pytest
from utils.yaml_handler import get_config
from apis.user_api import UserApi

@pytest.fixture(scope="session")
def env_config():
    """全局环境配置 fixture"""
    return get_config()

@pytest.fixture(scope="session")
def auth_user_api(env_config):
    """
    会话级 Fixture:完成用户登录,并返回已挂载 Authorization Token 的 API 客户端。
    整个测试 Session 生命周期只执行一次登录。
    """
    base_url = env_config["base_url"]
    timeout = env_config.get("timeout", 15)
    user_info = env_config["default_user"]

    # 初始化客户端
    api_client = UserApi(base_url=base_url, timeout=timeout)

    # 发起登录以获取 Token
    response = api_client.login(
        username=user_info["username"],
        password=user_info["password"]
    )
    assert response.status_code == 200, f"全局前置登录失败,状态码: {response.status_code}"
    
    resp_data = response.json()
    token = resp_data.get("data", {}).get("token")
    assert token, "响应中未能获取到有效 token"

    # 将 Token 挂载进 Session 的 headers
    api_client.set_auth_token(token)
    
    yield api_client
    
    # 拆卸阶段(可选):登出或会话清理
    api_client.session.close()

六、YAML 数据驱动与测试用例实战

为了让用例具备强大的覆盖能力与清晰的可读性,我们采用数据驱动测试(Data-Driven Testing)模式。在 data/user_cases.yaml 中设计具体用例输入与期望响应:

# data/user_cases.yaml
# 更新个人资料数据驱动用例集
update_profile_cases:
  - case_id: "USER_001"
    title: "正常修改用户昵称与邮箱"
    payload:
      nickname: "QA_Leader"
      email: "qa_leader@example.com"
    expect:
      http_status: 200
      business_code: 0
      msg: "success"

  - case_id: "USER_002"
    title: "邮箱格式不合法边界校验"
    payload:
      nickname: "QA_Leader"
      email: "illegal_email_format"
    expect:
      http_status: 400
      business_code: 1004
      msg: "invalid email format"

  - case_id: "USER_003"
    title: "昵称为空字符校验"
    payload:
      nickname: ""
      email: "valid@example.com"
    expect:
      http_status: 400
      business_code: 1002
      msg: "nickname cannot be empty"

在 cases/test_user.py 中编写测试用例。利用 @pytest.mark.parametrize 装饰器动态加载用例列表,通过依赖注入直接拿到前置准备好的 auth_user_api 实例:

# cases/test_user.py
import pytest
from utils.yaml_handler import load_yaml

# 读取测试数据
cases_data = load_yaml("data/user_cases.yaml").get("update_profile_cases", [])

class TestUserModule:

    def test_get_user_profile(self, auth_user_api):
        """测试获取个人资料:正向查询用例"""
        res = auth_user_api.get_profile()
        
        # 1. HTTP 状态码断言
        assert res.status_code == 200, f"预期 200,实际为 {res.status_code}"
        
        # 2. 业务状态码断言
        data = res.json()
        assert data.get("code") == 0, f"业务响应失败: {data.get('msg')}"
        
        # 3. 关键字段与类型断言
        user_info = data.get("data")
        assert isinstance(user_info, dict)
        assert "username" in user_info
        assert len(user_info["username"]) > 0

    @pytest.mark.parametrize(
        "case",
        cases_data,
        ids=[c["case_id"] + "-" + c["title"] for c in cases_data]
    )
    def test_update_user_profile(self, auth_user_api, case):
        """测试修改个人资料:数据驱动正向与异常覆盖"""
        payload = case["payload"]
        expect = case["expect"]

        # 发起接口请求
        res = auth_user_api.update_profile(
            nickname=payload["nickname"],
            email=payload["email"]
        )

        # 多级断言
        assert res.status_code == expect["http_status"], (
            f"用例 [{case['title']}] HTTP 状态码不匹配: "
            f"预期 {expect['http_status']}, 实际 {res.status_code}"
        )

        res_json = res.json()
        assert res_json.get("code") == expect["business_code"], (
            f"业务错误码不匹配: 预期 {expect['business_code']}, 实际 {res_json.get('code')}"
        )
        assert expect["msg"] in res_json.get("msg", ""), (
            f"提示文案不匹配: 预期包含 {expect['msg']}, 实际为 {res_json.get('msg')}"
        )

七、执行测试与报告生成

框架搭建完成后,在终端控制台或 CI/CD 流程中执行 pytest 命令即可自动发现并运行所有用例。

# 1. 运行全部用例并生成独立 HTML 报告
pytest

# 2. 仅运行指定测试文件
pytest cases/test_user.py

# 3. 按用例关键字筛选运行
pytest -k "test_update_user_profile"

执行后,pytest 会按照 pytest.ini 中设定的配置在 reports/ 目录下生成 report.html。控制台执行结果示例如下:

# 运行结果控制台示例输出
============================= test session starts ==============================
platform darwin -- Python 3.11.8, pytest-8.1.1, pluggy-1.4.0
rootdir: /path/to/api_test_framework
configfile: pytest.ini
testpaths: cases
plugins: html-4.1.1, metadata-3.1.1
collected 4 items

cases/test_user.py::TestUserModule::test_get_user_profile PASSED         [ 25%]
cases/test_user.py::TestUserModule::test_update_user_profile[USER_001-正常修改用户昵称与邮箱] PASSED [ 50%]
cases/test_user.py::TestUserModule::test_update_user_profile[USER_002-邮箱格式不合法边界校验] PASSED [ 75%]
cases/test_user.py::TestUserModule::test_update_user_profile[USER_003-昵称为空字符校验] PASSED [100%]

- Generated html report: file:///path/to/api_test_framework/reports/report.html -
============================== 4 passed in 0.86s ===============================

八、框架核心设计原则与避坑指南

许多团队在接口自动化框架搭建后,随着用例数量增长到数百甚至数千个,逐渐面临用例频频误报、执行极度缓慢、数据越积越多的困境。保持框架长期可靠运行必须恪守以下四项原则:

Python接口自动化测试框架搭建:pytest+requests实战
图 4:接口自动化测试框架最佳实践与避坑要点总结
避坑要点 1:警惕跨用例的隐式状态依赖
用例之间切忌存在执行时序依赖。例如“用例 B 查询用例 A 刚刚新增的商品 ID”。一旦由于测试失败重跑、按标记筛选执行或启用 pytest-xdist 进行多进程并发测试,执行顺序被打乱,用例 B 就会因为找不到依赖数据而直接报错。正确的做法是:用例 B 所需的数据由其前置 fixture 独立创建,并在测试完毕后自动销毁。
避坑要点 2:避免粗暴的全字段严格比对
很多初学者喜欢直接拿 res.json() == expect_json 做全量比对。在真实服务端响应中,通常包含 update_time、timestamp 或动态生成的 order_id,全量字符比对会导致用例极易脆弱报错。建议采用“状态码 + 核心业务 code + 关键字段类型与业务有效值”的分层断言方式。
避坑要点 3:脏数据闭环清理机制
测试用例对数据库的写操作(新增、修改)如果不加控制,长期运行会导致测试数据库膨胀,甚至引起唯一索引冲突导致后续测试失败。应充分利用 pytest fixture 的 yield 特性,在用例完成后自动调用对应模块的删除接口或数据库回滚清理,保持环境整洁。
相关专题:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表