在日常迭代中,接口变动频繁,人工回归耗时且容易遗漏。一套设计良好的接口自动化框架,能让测试用例从“一次性脚本”升级为“可复用资产”。
华测教育的课程体系中,接口自动化是核心模块之一,涵盖从 Requests 入门到框架封装、Jenkins 持续集成的完整链路。本文按照“数据驱动 → 核心封装 → 报告集成 → 无人值守”的路线,从零搭建一套可直接运行的接口自动化测试框架。
最终效果:测试用例写在 Excel 中,一条命令完成全部接口测试,自动生成 Allure 报告,并支持 Jenkins 流水线执行。
┌─────────────────────────────────────────────────────────────┐
│ 数据层:Excel 测试用例(接口信息 + 请求参数 + 预期结果) │
└─────────────────────┬───────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ 核心层:读取 Excel → 封装 Requests → 执行请求 │
│ → 断言校验(JSONPath/正则/状态码) │
└─────────────────────┬───────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ 报告层:Allure 生成结构化测试报告 │
└─────────────────────┬───────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ 集成层:Jenkins 流水线自动拉取代码 + 执行测试 + 发报告 │
└─────────────────────────────────────────────────────────────┘框架核心设计理念是数据与代码分离:Excel 中维护用例数据,Python 代码负责通用的请求封装和断言逻辑,修改用例无需改代码。
api_test_framework/
├── config/
│ └── config.yaml # 全局配置(域名、超时等)
├── data/
│ └── test_cases.xlsx # Excel 测试用例
├── common/
│ ├── excel_reader.py # 读取 Excel
│ ├── http_client.py # Requests 封装
│ └── assert_engine.py # 断言引擎
├── testcases/
│ └── test_api.py # pytest 测试入口
├── reports/ # 测试报告目录
├── conftest.py # pytest 钩子(Allure 配置)
├── requirements.txt
└── run.py # 执行入口依赖安装:
pip install pytest requests openpyxl allure-pytest pyyaml# config/config.yaml
base:
host: "https://api.example.com"
timeout: 10
headers:
Content-Type: "application/json"
# config/loader.py
import yaml
import os
class ConfigLoader:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
config_path = os.path.join(os.path.dirname(__file__), "config.yaml")
with open(config_path, "r", encoding="utf-8") as f:
cls._instance.config = yaml.safe_load(f)
return cls._instance
@property
def host(self):
return self.config["base"]["host"]
@property
def timeout(self):
return self.config["base"]["timeout"]
def get_headers(self):
return self.config.get("headers", {})Excel 模板设计:
用例ID | 模块 | 接口名称 | 方法 | URL | 请求头 | 请求体 | 预期状态码 | 断言方式 | 断言表达式 | 期望值 |
|---|---|---|---|---|---|---|---|---|---|---|
TC001 | 用户 | 登录 | POST | /login | {"token":"xxx"} | {"username":"test","password":"123"} | 200 | jsonpath | $.code | 0 |
# common/excel_reader.py
import openpyxl
from typing import List, Dict, Any
class ExcelReader:
def __init__(self, file_path: str):
self.file_path = file_path
def read_all_cases(self) -> List[Dict[str, Any]]:
"""读取 Excel 所有用例,返回字典列表"""
wb = openpyxl.load_workbook(self.file_path)
ws = wb.active
# 第一行为表头
headers = [cell.value for cell in ws[1]]
cases = []
for row in ws.iter_rows(min_row=2, values_only=True):
case = dict(zip(headers, row))
# 跳过空行
if not case.get("用例ID"):
continue
# 解析 JSON 格式的请求头和请求体
if case.get("请求头"):
case["请求头"] = eval(case["请求头"]) if isinstance(case["请求头"], str) else {}
if case.get("请求体"):
case["请求体"] = eval(case["请求体"]) if isinstance(case["请求体"], str) else {}
if case.get("断言表达式"):
case["断言表达式"] = str(case["断言表达式"])
cases.append(case)
wb.close()
return cases
def get_case_by_id(self, case_id: str) -> Dict:
"""根据用例ID获取单条用例"""
cases = self.read_all_cases()
for case in cases:
if str(case.get("用例ID")) == case_id:
return case
return {}# common/http_client.py
import requests
import json
from typing import Optional, Dict, Any
class HttpClient:
def __init__(self, base_url: str, timeout: int = 10):
self.base_url = base_url.rstrip("/")
self.timeout = timeout
self.session = requests.Session()
def _build_url(self, path: str) -> str:
"""拼接完整 URL"""
if path.startswith("http"):
return path
return f"{self.base_url}/{path.lstrip('/')}"
def _parse_body(self, body: Any) -> Dict:
"""将字符串形式的 body 解析为字典"""
if isinstance(body, dict):
return body
if isinstance(body, str):
try:
return json.loads(body)
except json.JSONDecodeError:
return {}
return {}
def request(
self,
method: str,
path: str,
headers: Optional[Dict] = None,
body: Optional[Dict] = None
) -> requests.Response:
"""统一请求入口"""
url = self._build_url(path)
headers = headers or {}
body = self._parse_body(body)
# 合并默认 headers
default_headers = {"Content-Type": "application/json"}
default_headers.update(headers)
method = method.upper()
if method == "GET":
return self.session.get(url, params=body, headers=default_headers, timeout=self.timeout)
elif method == "POST":
return self.session.post(url, json=body, headers=default_headers, timeout=self.timeout)
elif method == "PUT":
return self.session.put(url, json=body, headers=default_headers, timeout=self.timeout)
elif method == "DELETE":
return self.session.delete(url, json=body, headers=default_headers, timeout=self.timeout)
else:
raise ValueError(f"不支持的请求方法: {method}")支持状态码断言和 JSONPath 断言两种方式。
# common/assert_engine.py
import json
from typing import Any, Dict
from jsonpath import jsonpath # pip install jsonpath
class AssertEngine:
@staticmethod
def assert_status_code(response, expected_code: int) -> bool:
"""断言状态码"""
return response.status_code == expected_code
@staticmethod
def assert_jsonpath(response, expr: str, expected_value: Any) -> bool:
"""
JSONPath 断言
示例: expr="$.code", expected_value=0
"""
try:
data = response.json()
result = jsonpath(data, expr)
if not result:
return False
# 如果期望值是字符串,转成字符串比较;否则直接比较
if isinstance(expected_value, str) and expected_value.isdigit():
expected_value = int(expected_value)
return result[0] == expected_value
except Exception as e:
print(f"JSONPath 断言执行失败: {e}")
return False
@staticmethod
def assert_regex(response, pattern: str, expected_value: str) -> bool:
"""正则断言"""
import re
match = re.search(pattern, response.text)
if not match:
return False
return match.group(0) == expected_value
def assert_case(self, response, case: Dict) -> tuple:
"""
执行单条用例的所有断言
返回: (是否通过, 详细结果)
"""
results = []
all_passed = True
# 1. 状态码断言
expected_code = case.get("预期状态码", 200)
status_ok = self.assert_status_code(response, expected_code)
results.append(f"状态码: {response.status_code} (期望 {expected_code}) {'✅' if status_ok else '❌'}")
if not status_ok:
all_passed = False
# 2. JSONPath 断言
if case.get("断言方式") == "jsonpath":
expr = case.get("断言表达式")
expected = case.get("期望值")
if expr and expected:
json_ok = self.assert_jsonpath(response, expr, expected)
results.append(f"JSONPath: {expr} -> {expected} {'✅' if json_ok else '❌'}")
if not json_ok:
all_passed = False
# 3. 正则断言
if case.get("断言方式") == "regex":
pattern = case.get("断言表达式")
expected = case.get("期望值")
if pattern and expected:
regex_ok = self.assert_regex(response, pattern, expected)
results.append(f"正则: {pattern} {'✅' if regex_ok else '❌'}")
if not regex_ok:
all_passed = False
return all_passed, "\n".join(results)# conftest.py
import pytest
import allure
from common.excel_reader import ExcelReader
from common.http_client import HttpClient
from config.loader import ConfigLoader
@pytest.fixture(scope="session")
def config():
return ConfigLoader()
@pytest.fixture(scope="session")
def http_client(config):
return HttpClient(config.host, config.timeout)
@pytest.fixture
def test_cases():
"""加载所有测试用例"""
reader = ExcelReader("data/test_cases.xlsx")
return reader.read_all_cases()
# testcases/test_api.py
import pytest
import allure
from common.assert_engine import AssertEngine
from common.excel_reader import ExcelReader
@allure.feature("接口自动化测试")
class TestAPI:
@pytest.mark.parametrize(
"case",
ExcelReader("data/test_cases.xlsx").read_all_cases(),
ids=lambda case: f"{case.get('用例ID')}_{case.get('接口名称')}"
)
@allure.story("执行 Excel 中的测试用例")
def test_api_case(self, http_client, case):
"""
执行单条 Excel 用例
"""
with allure.step(f"请求: {case.get('方法')} {case.get('URL')}"):
response = http_client.request(
method=case.get("方法", "GET"),
path=case.get("URL"),
headers=case.get("请求头", {}),
body=case.get("请求体", {})
)
# 记录请求和响应
allure.attach(
f"请求: {case.get('方法')} {case.get('URL')}\n请求体: {case.get('请求体')}",
name="请求信息",
attachment_type=allure.attachment_type.TEXT
)
allure.attach(
f"状态码: {response.status_code}\n响应体: {response.text[:500]}",
name="响应信息",
attachment_type=allure.attachment_type.TEXT
)
with allure.step("执行断言"):
engine = AssertEngine()
passed, detail = engine.assert_case(response, case)
assert passed, f"断言失败:\n{detail}"# 安装依赖
pip install -r requirements.txt
# 执行测试
python run.py
# 或使用 pytest 命令
pytest testcases/ -v --alluredir=reports/allure_raw# 安装 allure(Mac)
brew install allure
# 生成并打开报告
allure generate reports/allure_raw -o reports/allure_html --clean
allure open reports/allure_htmlAllure 报告会展示:
创建 Jenkinsfile 实现自动化执行:
pipeline {
agent any
environment {
PYTHONPATH = "${env.WORKSPACE}"
}
stages {
stage('代码拉取') {
steps {
git branch: 'main',
url: 'https://github.com/your/api_test_framework.git'
}
}
stage('环境准备') {
steps {
sh 'pip install -r requirements.txt'
}
}
stage('执行接口测试') {
steps {
sh '''
pytest testcases/ -v --alluredir=reports/allure_raw
'''
}
post {
always {
// 生成 Allure 报告
allure([
includeProperties: false,
jdk: '',
properties: [],
reportBuildPolicy: 'ALWAYS',
results: [[path: 'reports/allure_raw']]
])
}
}
}
}
post {
failure {
emailext (
subject: "接口自动化测试失败: ${env.JOB_NAME} - ${env.BUILD_NUMBER}",
body: "请查看测试报告: ${env.BUILD_URL}allure",
to: "qa-team@example.com"
)
}
}
}配置 Jenkins 定时执行(如每日凌晨 2 点):
H 2 * * *这样就实现了无人值守的接口自动化测试:Jenkins 定时拉取最新代码 → 执行测试 → 生成 Allure 报告 → 失败时邮件通知。
接口依赖是常见场景(如登录后获取 token,后续接口携带 token)。可在 Excel 中增加“依赖接口”字段,框架自动维护变量池:
# 在 http_client 中增加变量池
class HttpClient:
def __init__(self, ...):
self.variables = {} # 存储全局变量
def set_var(self, key, value):
self.variables[key] = value
def get_var(self, key):
return self.variables.get(key)用例中支持 ${token} 占位符,执行前替换。
对于 MD5、AES 加密接口,可在请求前调用加密函数:
def encrypt_body(body: dict, secret: str) -> dict:
# 根据业务规则对 body 进行加密
return body参考 RTC 闭环覆盖率的思路,可将测试用例与需求、代码建立追溯关系,在测试报告中加入覆盖率指标。
指标 | 人工测试 | 框架自动化 |
|---|---|---|
单轮回归耗时 | 2-4 小时 | 3-5 分钟 |
用例维护成本 | 每次改代码需重新测试 | 仅修改 Excel |
报告生成 | 手工整理 | Allure 自动生成 |
定时执行 | 需要人工触发 | Jenkins 无人值守 |
核心收益:将重复性的回归测试交给框架,测试人员将精力集中在用例设计和缺陷分析上。
参考资源:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。