设计一个基于Requests和Allure的接口自动化测试框架,要解决三个问题:易编写、好维护、报告清晰。
一、框架选型
Requests:简洁的 HTTP 客户端,支持 Session 保持、SSL 校验、连接池,学习成本极低。
Allure:测试报告框架,可生成清晰直观的 HTML 报告,支持步骤、附件、分级标签,与 Pytest 原生集成。
Pytest:用例组织与运行引擎,提供 fixture、参数化、标记、钩子等能力。
整体风格:分层解耦 + API Object 模式 + 数据驱动。
二、整体架构分层
从上到下共分为四层:
测试用例层
基于Pytest编写的测试文件,使用 @allure.step、@allure.feature 等装饰器描述业务,直接调用接口对象层提供的方法。
接口对象层(API Object)
将系统接口按模块封装成类(如UserAPI、OrderAPI),对外暴露业务动作(login、create_order),内部统一使用基础工具层的请求客户端发送HTTP请求。
基础工具层(Utils)
提供通用能力:Requests Session 封装(自动处理Token、日志、重试、超时)、日志模块、断言封装、数据库辅助等。
数据与配置层
存放多环境配置文件(config.yaml)、测试数据文件(YAML/CSV)以及环境变量,供各层读取使用。
三、目录结构说明
项目根目录为api-test-framework/,主要子目录和文件如下:
config/
存放配置文件,包含config.yaml(多环境全局配置)和env.yaml(环境变量覆盖)。
data/
存放测试数据,例如 login_data.yaml、order_data.csv 等。
apis/
接口对象层代码,包含 __init__.py、base_api.py(公共请求基类)、user_api.py、order_api.py 等模块。
utils/
工具层代码,包含 __init__.py、request_client.py(Session 封装)、logger.py、assertion.py、db_helper.py。
testcases/
测试用例目录,包含 conftest.py(全局 fixture)、test_login.py、test_order.py 等测试文件。
reports/
用于存放生成的 Allure 原始结果和 HTML 报告。
pytest.ini
Pytest 配置文件。
requirements.txt
项目依赖列表。
run.py
统一运行入口脚本。
四、模块实现
1. 配置管理(config/config.yaml)
yaml
env: test
test:
base_url: "https://test-api.example.com"
timeout: 10
db:
host: "127.0.0.1"
user: "tester"
password: "secret"
database: "testdb"
prod:
base_url: "https://api.example.com"
timeout: 5
加载配置工具(utils/config_reader.py)使用 pyyaml 读取并合并环境变量。
2. 日志模块(utils/logger.py)
python
import logging
import sys
from pathlib import Path
def get_logger(name=__name__):
logger = logging.getLogger(name)
logger.setLevel(logging.DEBUG)
if not logger.handlers:
handler = logging.StreamHandler(sys.stdout)
fmt = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
handler.setFormatter(fmt)
logger.addHandler(handler)
return logger
3. 请求客户端封装(utils/request_client.py)
封装 requests.Session,实现自动挂载 Token、SSL 忽略或证书、连接重试、请求/响应日志记录。
python
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from utils.logger import get_logger
logger = get_logger(__name__)
class RequestClient:
def __init__(self, base_url, timeout=10):
self.base_url = base_url.rstrip('/')
self.timeout = timeout
self.session = requests.Session()
retries = Retry(total=3, backoff_factor=1, status_forcelist=[500, 502, 503])
self.session.mount('http://', HTTPAdapter(max_retries=retries))
self.session.mount('https://', HTTPAdapter(max_retries=retries))
self.session.headers.update({'Content-Type': 'application/json'})
def _log_request(self, method, url, **kwargs):
logger.info(f"Request: {method.upper()} {url} | params={kwargs.get('params')} | json={kwargs.get('json')}")
def _log_response(self, response):
logger.info(f"Response: {response.status_code} | time={response.elapsed.total_seconds():.3f}s | body={response.text[:500]}")
def send_request(self, method, path, **kwargs):
url = f"{self.base_url}{path}"
kwargs.setdefault('timeout', self.timeout)
self._log_request(method, url, **kwargs)
try:
resp = self.session.request(method, url, **kwargs)
self._log_response(resp)
return resp
except Exception as e:
logger.error(f"Request failed: {e}")
raise
4. 通用断言封装(utils/assertion.py)
python
import jsonpath
from jsonschema import validate
def assert_status_code(response, expected_code):
assert response.status_code == expected_code, \
f"Expected {expected_code}, got {response.status_code}, body: {response.text}"
def assert_json_value(response, expr, expected_value):
actual = jsonpath.jsonpath(response.json(), expr)
assert actual, f"JsonPath '{expr}' not found"
assert actual[0] == expected_value, f"Expected {expected_value}, got {actual[0]}"
5. API Object 基类(apis/base_api.py)
python
class BaseAPI:
def __init__(self, client: 'RequestClient'):
self.client = client
6. 具体接口封装(apis/user_api.py)
python
import allure
from apis.base_api import BaseAPI
class UserAPI(BaseAPI):
@allure.step("登录接口")
def login(self, username, password):
path = "/api/v1/login"
payload = {"username": username, "password": password}
return self.client.send_request("POST", path, json=payload)
@allure.step("获取用户信息")
def get_user_info(self, token):
headers = {"Authorization": f"Bearer {token}"}
return self.client.send_request("GET", "/api/v1/user/me", headers=headers)
7. 全局 Fixture(testcases/conftest.py)
python
import pytest
import allure
from utils.request_client import RequestClient
from apis.user_api import UserAPI
from utils.config_reader import load_config
@pytest.fixture(scope="session")
def config():
return load_config("config/config.yaml")
@pytest.fixture(scope="session")
def client(config):
env = config['env']
base_url = config[env]['base_url']
return RequestClient(base_url, timeout=config[env]['timeout'])
@pytest.fixture(scope="session")
def admin_token(client, config):
user_api = UserAPI(client)
resp = user_api.login("admin", "admin123")
assert resp.status_code == 200
token = resp.json()["data"]["token"]
client.session.headers.update({"Authorization": f"Bearer {token}"})
with allure.step("获取管理员Token"):
allure.attach(str(token), "Admin Token")
return token
@pytest.fixture
def user_api(client):
return UserAPI(client)
8. 测试用例编写(testcases/test_login.py)
python
import allure
import pytest
@allure.feature("用户模块")
class TestLogin:
@allure.story("登录成功")
@allure.severity(allure.severity_level.BLOCKER)
@pytest.mark.parametrize("username,password", [
("admin", "admin123"),
("user1", "pass1")
])
def test_login_success(self, user_api, username, password):
resp = user_api.login(username, password)
with allure.step("校验状态码200"):
assert resp.status_code == 200
with allure.step("校验返回token不为空"):
assert resp.json()["data"]["token"] != ""
@allure.story("登录失败-密码错误")
def test_login_fail(self, user_api):
resp = user_api.login("admin", "wrong_pass")
with allure.step("校验状态码401"):
assert resp.status_code == 401
with allure.step("校验错误信息"):
assert "invalid" in resp.json()["message"]
9. Allure配置运行
安装依赖:
bash
pip install requests pyyaml pytest allure-pytest jsonpath-ng jsonschema
# 还需下载 allure 命令行工具并添加到 PATH
配置 pytest.ini:
ini
[pytest]
addopts = -v -s --alluredir=reports/allure_raw
testpaths = testcases
运行并生成报告:
bash
pytest
allure generate reports/allure_raw -o reports/allure_html --clean
allure open reports/allure_html
10. 统一运行入口(run.py)
python
import pytest
import os
import sys
if __name__ == "__main__":
exit_code = pytest.main([
"-v", "-s",
"--alluredir=reports/allure_raw",
"testcases"
])
os.system("allure generate reports/allure_raw -o reports/allure_html --clean")
sys.exit(exit_code)
五、扩展设计要点
数据驱动增强
可将测试数据从 YAML/Excel 读取并参数化,例如:
python
def load_yaml_data(file_path):
import yaml
with open(file_path, encoding='utf-8') as f:
return yaml.safe_load(f)
@pytest.mark.parametrize("case", load_yaml_data("data/login_data.yaml"))
def test_login_with_data(self, user_api, case):
resp = user_api.login(case["username"], case["password"])
assert resp.status_code == case["expected_status"]
多环境切换
通过命令行参数 --env=prod 传入,conftest动态加载配置。
接口间的依赖处理
避免用例强依赖,使用fixture创建前置数据,并在yield后清理;或利用依赖注入共享资源。
数据库与Mock
数据库工具:在utils中封装pymysql/sqlalchemy,在fixture中提供数据准备。
Mock服务:使用responses库或WireMock模拟第三方依赖,在fixture中启动与销毁。
Allure进阶
步骤中截图/附件:allure.attach(response.text, "响应", allure.attachment_type.JSON)
添加环境信息:创建 environment.properties 放到 allure 结果目录。
使用 @allure.tag、@allure.epic、@allure.link 丰富报告元数据。
这套框架的优势:
高内聚低耦合:请求、业务、数据、用例分离,修改基类不影响用例。
复用性强:API Object 层可被任何用例调用,减少重复代码。
报告即文档:Allure 的 Feature/Story/Step 与业务语义对应,非技术人员也能看懂。
易于维护:新增接口只需实现 API 类和用例,配置与数据独立管理。