为什么选择离开舒适区?

Postman无疑是API测试的绝佳入门工具,直观的GUI让它易于上手。但当面临成百上千个接口、复杂的测试场景、CI/CD集成需求时,GUI操作变得笨重、难以维护和协作。而代码化(Code-based)的自动化框架,才是通往高效、可靠和可扩展的必经之路。

本文将手把手带你用Python的Requests库,从一个简单的请求开始,逐步构建一个结构清晰、功能完备的高可用接口自动化框架。

第一部分:为什么是Python + Requests?
  1. 极致简洁:Requests库让HTTP请求变得像普通函数调用一样简单,远比原生urllib优雅。
  2. 完全掌控:代码意味着无限的可能性。你可以轻松地集成数据驱动、生成动态参数、连接数据库、定制报告、接入CI/CD(如Jenkins)。
  3. 易于维护和版本控制:脚本文件(.py)可以用Git等工具进行版本管理,轻松追溯变更、协作开发。
  4. 强大的生态系统:Pytest(测试框架)、Allure(精美报告)、JSONPath(JSON解析)等众多库能与Requests无缝集成。
第二部分:四步搭建你的专属框架

我们的目标是构建一个结构分明、易于扩展的框架。最终目录结构将如下所示:

api_framework/
├── common/          # 公共模块
│   ├── __init__.py
│   ├── request_client.py  # 封装的请求客户端
│   └── logger.py    # 日志模块
├── config/          # 配置管理
│   ├── __init__.py
│   └── config.py
├── test_data/       # 测试数据
│   ├── __init__.py
│   └── user_data.py
├── test_cases/      # 测试用例
│   ├── __init__.py
│   └── test_user_api.py
├── reports/         # 测试报告(自动生成)
└── run_tests.py     # 测试入口文件

第一步:基础封装——打造你的“超级Requests客户端”

目标:封装 Requests ,自动处理 base_url 、日志、并返回统一的响应格式。

创建 common/request_client.py :

import requests
import json
from common.logger import get_logger

logger = get_logger(__name__)

class RequestClient:
    def __init__(self, base_url=""):
        self.base_url = base_url
        # 创建一个会话对象,可以自动保持cookies
        self.session = requests.Session()
        # 设置公共请求头
        self.session.headers.update({
            "Content-Type": "application/json; charset=utf-8"
        })

    def _request(self, method, url, **kwargs):
        """核心请求方法"""
        # 拼接完整的请求URL
        full_url = self.base_url + url
        logger.info(f"请求方法: {method}")
        logger.info(f"请求URL: {full_url}")
        if 'json' in kwargs:
            logger.info(f"请求体: {json.dumps(kwargs['json'], indent=2, ensure_ascii=False)}")
        if 'params' in kwargs:
            logger.info(f"请求参数: {kwargs['params']}")

        # 发送请求
        response = self.session.request(method, full_url, **kwargs)
        
        # 记录响应信息
        logger.info(f"响应状态码: {response.status_code}")
        try:
            logger.info(f"响应体: {json.dumps(response.json(), indent=2, ensure_ascii=False)}")
        except json.JSONDecodeError:
            logger.info(f"响应体(非JSON): {response.text}")

        # 返回一个统一的响应对象,包含更多有用信息
        return {
            "status_code": response.status_code,
            "headers": response.headers,
            "body": response.json() if 'application/json' in response.headers.get('Content-Type', '') else response.text,
            "original_response": response # 保留原始响应对象以备不时之需
        }

    # 定义便捷方法
    def get(self, url, params=None, **kwargs):
        return self._request('GET', url, params=params, **kwargs)

    def post(self, url, json_data=None, **kwargs):
        return self._request('POST', url, json=json_data, **kwargs)

    def put(self, url, json_data=None, **kwargs):
        return self._request('PUT', url, json=json_data, **kwargs)

    def delete(self, url, **kwargs):
        return self._request('DELETE', url, **kwargs)

创建 common/logger.py :

import logging
import os

def get_logger(name, level=logging.INFO):
    """创建一个格式化的logger"""
    logger = logging.getLogger(name)
    logger.setLevel(level)

    # 避免重复添加handler
    if not logger.handlers:
        # 创建控制台handler
        ch = logging.StreamHandler()
        formatter = logging.Formatter(
            '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
        )
        ch.setFormatter(formatter)
        logger.addHandler(ch)
    return logger

第二步:配置管理——告别硬编码

目标:将环境配置(如测试/生产环境URL)与代码分离。

创建 config/config.py :

import os

class Config:
    """配置类,支持从环境变量或默认值读取配置"""
    # 基础URL,可通过环境变量覆盖
    BASE_URL = os.getenv("API_BASE_URL", "https://api.your-test-server.com/v1")
    
    # 数据库配置(如需)
    DB_HOST = os.getenv("DB_HOST", "localhost")
    DB_USER = os.getenv("DB_USER", "test_user")

config = Config()

第三步:测试数据分离——实现数据驱动

目标:将测试用例与测试数据分离,使用例更简洁,数据易于维护。

创建 test_data/user_data.py :

# 正常用例数据
VALID_USER_DATA = {
    "create_user": {"name": "悟空", "email": "wukong@xyj.com"},
    "login_success": {"username": "admin", "password": "123456"}
}

# 异常用例数据
INVALID_USER_DATA = {
    "login_missing_password": {"username": "admin"},
    "login_wrong_password": {"username": "admin", "password": "wrong"}
}

第四步:编写真正的测试用例——使用Pytest

目标:利用Pytest框架的强大功能(夹具、参数化、断言)编写清晰、健壮的测试。

创建 test_cases/test_user_api.py :

import pytest
import allure
from config.config import config
from common.request_client import RequestClient

# 使用pytest fixture在用例开始时自动初始化客户端
@pytest.fixture(scope="class")
def api_client():
    client = RequestClient(base_url=config.BASE_URL)
    yield client # 提供客户端给测试类使用
    # 测试结束后可以在这里做一些清理工作,比如退出登录

@allure.epic("用户管理模块")
@allure.feature("用户登录")
class TestUserLogin:
    """测试用户登录相关接口"""

    @allure.story("成功登录")
    @allure.title("使用正确的用户名和密码可以成功登录")
    def test_login_success(self, api_client):
        # 准备测试数据
        login_data = {"username": "admin", "password": "123456"}
        
        # 执行请求
        response = api_client.post("/auth/login", json_data=login_data)
        
        # 断言
        assert response["status_code"] == 200
        assert response["body"]["code"] == 0
        assert "token" in response["body"]["data"]
        assert response["body"]["data"]["username"] == login_data["username"]

    @allure.story("登录失败")
    @allure.title("密码错误时登录失败")
    @pytest.mark.parametrize("case_name, login_data, expected_code, expected_msg", [
        ("缺失密码", {"username": "admin"}, 400, "密码不能为空"),
        ("密码错误", {"username": "admin", "password": "wrong"}, 400, "用户名或密码错误"),
    ])
    def test_login_failure(self, api_client, case_name, login_data, expected_code, expected_msg):
        with allure.step(f"测试场景:{case_name}"):
            response = api_client.post("/auth/login", json_data=login_data)
            assert response["status_code"] == expected_code
            assert response["body"]["msg"] == expected_msg

@allure.feature("用户CRUD")
class TestUserCRUD:
    """测试用户的增删改查"""

    def test_create_user(self, api_client):
        user_data = {"name": "新用户", "email": "new@example.com"}
        response = api_client.post("/users", json_data=user_data)
        assert response["status_code"] == 201
        created_user = response["body"]["data"]
        assert created_user["name"] == user_data["name"]
        # 通常这里会保存创建的用户ID,用于后续的查询、更新、删除测试
        # 这体现了用例之间的依赖和数据传递
        return created_user["id"]

    # 这个测试依赖于上一个测试创建的用户
    def test_get_user_by_id(self, api_client):
        # 在实际框架中,这里应该获取上一步创建的user_id
        # user_id = self.test_create_user(api_client)
        user_id = 123 # 假设一个存在的ID,或从配置中读取
        response = api_client.get(f"/users/{user_id}")
        assert response["status_code"] == 200

第五步:运行与报告——让结果一目了然

创建 run_tests.py :

import pytest
import os
import shutil

if __name__ == "__main__":
    # 清理之前的报告
    report_dir = "./reports"
    if os.path.exists(report_dir):
        shutil.rmtree(report_dir)
    
    # 运行pytest,生成Allure原始数据
    # 1. 运行测试
    pytest.main(["-v", "-s", "test_cases/", "--alluredir=./reports/allure_raw"])
    
    # 2. 生成HTML报告(需要先安装Allure命令行工具)
    # 你可以在CI/CD中执行这一步
    os.system("allure generate ./reports/allure_raw -o ./reports/allure_html --clean")
    
    print("测试完成!打开 `reports/allure_html/index.html` 查看精美报告。")
第三部分:框架的高阶扩展能力

你的框架已经具备雏形,但它的强大之处在于可扩展性:

  1. 全局认证处理:在RequestClient中自动为所有请求添加Token。
  2. 数据驱动@pytest.mark.parametrize:从Excel、JSON或YAML文件中读取大量测试数据。
  3. 数据库校验:接口调用后,自动查询数据库验证数据是否正确落库。
  4. Mock服务:在依赖服务不稳定时,使用unittest.mock或pytest-mock进行隔离测试。
  5. CI/CD集成:在Jenkins/GitLab CI中一键执行测试,并将Allure报告发布到服务器。
总结:从Postman到专业框架的蜕变

通过以上步骤,你已经不再是一个简单的API调用者,而是一个自动化框架的设计师。

特性PostmanPython + Requests 框架
维护性集合/环境变量,协作尚可代码版本控制,极致协作
灵活性GUI操作,有限代码实现,无限可能
扩展性有限,依赖Newman等CLI工具极强,无缝接入Python生态
报告基础HTML报告Allure等专业级可视化报告
集成性需要额外配置天生为CI/CD设计

更多推荐