一、项目测试背景
本次测试的博客系统后端 API 基于 Spring Boot 框架搭建,提供用户认证、博客管理、用户信息查询等核心功能。为确保 API 接口功能稳定、数据准确性和安全性,对系统开展了全面的 API 自动化测试。
在自动化测试中,我们选择 Python + Pytest + Requests + Allure 报告 + Logging 日志 + PyYAML + jsonschema 技术栈,要求:
- 覆盖用户登录 、博客列表 、博客详情 、博客新增 、用户信息 、作者信息六大模块
- 每个模块用例数控制在合理范围,采用等价类划分、边界值分析等方法设计
- 日志分级输出(DEBUG/INFO/WARNING/ERROR),三个日志文件,按大小自动分割(10MB/文件)
- 采用数据驱动测试模式,测试数据与测试代码分离,使用 YAML 文件管理测试数据
- 使用 JSON Schema 校验响应结构的正确性
- 全局 Fixture 管理登录 Token 和测试数据,提高测试效率
本文首先给出手动测试用例设计(表格形式),再详细阐述自动化测试的实现方式。
二、手动测试用例设计
2.1 用户登录接口测试用例(共 8 条)
| 用例编号 | 用例名称 | 优先级 | 操作步骤 | 预期结果 |
|---|---|---|---|---|
| TC-LOGIN-01 | 正常登录 | P0 | POST /user/login,参数username=zhangsan, password=123456 | 返回 code=SUCCESS,data 返回 token |
| TC-LOGIN-02 | 用户名为空 | P0 | POST /user/login,参数username="", password=123456 | 返回 code=FAIL,errMsg="账号或密码不能为空" |
| TC-LOGIN-03 | 密码为空 | P0 | POST /user/login,参数username=zhangsan, password="" | 返回 code=FAIL,errMsg="账号或密码不能为空" |
| TC-LOGIN-04 | 用户名密码均空 | P1 | POST /user/login,参数username="", password="" | 返回 code=FAIL,errMsg="账号或密码不能为空" |
| TC-LOGIN-05 | 用户名错误 | P1 | POST /user/login,参数username=wrong, password=123456 | 返回 code=FAIL,errMsg="用户不存在" |
| TC-LOGIN-06 | 密码错误 | P1 | POST /user/login,参数username=zhangsan, password=wrong | 返回 code=FAIL,errMsg="密码错误" |
| TC-LOGIN-07 | 超长用户名 | P2 | POST /user/login,参数username=128位超长字符串, password=123456 | 返回 code=FAIL,errMsg="用户不存在" |
| TC-LOGIN-08 | SQL注入攻击 | P2 | POST /user/login,参数username='OR 1=1--, password=123456 | 返回 code=FAIL,errMsg="用户不存在" |
2.2 获取博客列表接口测试用例(共 4 条)
| 用例编号 | 用例名称 | 优先级 | 操作步骤 | 预期结果 |
|---|---|---|---|---|
| TC-LIST-01 | 正常获取列表 | P0 | GET /blog/getList,请求头包含有效token | 返回 code=SUCCESS,data 为博客列表数组 |
| TC-LIST-02 | Token缺失 | P0 | GET /blog/getList,无请求头 | 返回 401 状态码 |
| TC-LIST-03 | Token格式错误 | P1 | GET /blog/getList,token为无效字符串 | 返回 401 状态码 |
| TC-LIST-04 | Token过期 | P1 | GET /blog/getList,token为过期token | 返回 401 状态码 |
2.3 获取博客详情接口测试用例(共 5 条)
| 用例编号 | 用例名称 | 优先级 | 操作步骤 | 预期结果 |
|---|---|---|---|---|
| TC-DETAIL-01 | 正常获取详情 | P0 | GET /blog/getBlogDetail?blogId=有效ID,请求头包含有效token | 返回 code=SUCCESS,data 包含博客详情 |
| TC-DETAIL-02 | Token缺失 | P0 | GET /blog/getBlogDetail?blogId=有效ID,无请求头 | 返回 401 状态码 |
| TC-DETAIL-03 | blogId参数缺失 | P1 | GET /blog/getBlogDetail,请求头包含有效token | 返回 code=FAIL,errMsg="内部错误, 请联系管理员" |
| TC-DETAIL-04 | 非法blogId格式 | P1 | GET /blog/getBlogDetail?blogId=abc,请求头包含有效token | 返回 code=FAIL,errMsg="内部错误, 请联系管理员" |
| TC-DETAIL-05 | 不存在的blogId | P1 | GET /blog/getBlogDetail?blogId=99999,请求头包含有效token | 返回 code=FAIL,errMsg="内部错误, 请联系管理员" |
2.4 新增博客接口测试用例(共 6 条)
| 用例编号 | 用例名称 | 优先级 | 操作步骤 | 预期结果 |
|---|---|---|---|---|
| TC-ADD-01 | 正常新增博客 | P0 | POST /blog/add,请求头包含有效token,body包含title和content | 返回 code=SUCCESS,data 返回新增博客信息 |
| TC-ADD-02 | Token缺失 | P0 | POST /blog/add,无请求头 | 返回 401 状态码 |
| TC-ADD-03 | 标题缺失 | P1 | POST /blog/add,请求头包含有效token,title为空 | 返回 code=SUCCESS,data为false |
| TC-ADD-04 | 内容缺失 | P1 | POST /blog/add,请求头包含有效token,content为空 | 返回 code=SUCCESS,data为false |
| TC-ADD-05 | 超长标题 | P2 | POST /blog/add,请求头包含有效token,title为超长字符串 | 返回 code=SUCCESS,data为false |
| TC-ADD-06 | XSS攻击内容 | P2 | POST /blog/add,请求头包含有效token,content含 |
2.5 获取用户信息接口测试用例(共 3 条)
| 用例编号 | 用例名称 | 优先级 | 操作步骤 | 预期结果 |
|---|---|---|---|---|
| TC-USER-01 | 正常获取用户信息 | P0 | GET /user/getUserInfo,请求头包含有效token | 返回 code=SUCCESS,data 包含用户信息 |
| TC-USER-02 | Token缺失 | P0 | GET /user/getUserInfo,无请求头 | 返回 401 状态码 |
| TC-USER-03 | Token篡改 | P1 | GET /user/getUserInfo,请求头包含篡改后的token | 返回 401 状态码 |
2.6 获取作者信息接口测试用例(共 5 条)
| 用例编号 | 用例名称 | 优先级 | 操作步骤 | 预期结果 |
|---|---|---|---|---|
| TC-AUTHOR-01 | 正常获取作者信息 | P0 | GET /user/getAuthorInfo?blogId=有效ID,请求头包含有效token | 返回 code=SUCCESS,data 包含作者信息 |
| TC-AUTHOR-02 | Token缺失 | P0 | GET /user/getAuthorInfo?blogId=有效ID,无请求头 | 返回 401 状态码 |
| TC-AUTHOR-03 | blogId参数缺失 | P1 | GET /user/getAuthorInfo,请求头包含有效token | 返回 code=FAIL,errMsg="内部错误, 请联系管理员" |
| TC-AUTHOR-04 | 非法blogId格式 | P1 | GET /user/getAuthorInfo?blogId=abc,请求头包含有效token | 返回 code=FAIL,errMsg="内部错误, 请联系管理员" |
| TC-AUTHOR-05 | 不存在的blogId | P1 | GET /user/getAuthorInfo?blogId=99999,请求头包含有效token | 返回 code=FAIL,errMsg="内部错误, 请联系管理员" |
三、自动化测试设计
3.1 技术栈与版本
| 组件 | 版本 | 说明 |
|---|---|---|
| Python | 3.10+ | 编程语言 |
| Requests | ≥2.32.0 | HTTP 请求库 |
| Pytest | ≥8.3.2 | 测试框架 |
| pytest-html | 3.2.0 | HTML 测试报告 |
| allure-pytest | ≥2.13.5 | Allure 报告适配 |
| PyYAML | ≥6.0.2 | YAML 解析 |
| jsonschema | ≥4.23.0 | JSON Schema 校验 |
3.2 环境搭建与目录结构
3.2.1 安装依赖
项目根目录 requirements.txt 内容如下:
text
# HTTP请求库
requests>=2.32.0
# pytest测试框架
pytest>=8.3.2
# pytest-html - 生成HTML测试报告
pytest-html==3.2.0
# allure-pytest - Allure测试报告
allure-pytest>=2.13.5
# PyYAML - YAML文件解析
PyYAML>=6.0.2
# jsonschema - JSON结构校验
jsonschema>=4.23.0
执行:
bash
pip install -r requirements.txt
3.2.2 项目目录结构
ApiAutoTest/
│
├── requirements.txt # 项目依赖
├── pytest.ini # Pytest配置(日志、插件、标记、命令行参数)
├── conftest.py # 全局Fixture:valid_token, blog_id (session级别)
│
├── config/
│ └── settings.py # BASE_URL, 超时时间, 日志级别等常量
│
├── logs/ # 日志目录(自动创建,按大小分割 10MB/文件)
│ ├── all.log # 所有日志(DEBUG及以上)
│ ├── info.log # INFO级别日志
│ └── error.log # ERROR级别日志
│
├── reports/ # Allure报告目录
│ ├── allure_results/ # pytest运行时生成(--alluredir)
│ └── allure_report/ # allure generate生成的HTML
│
├── schema/ # jsonschema校验文件
│ ├── login_schema.json
│ ├── blog_list_schema.json
│ ├── add_blog_schema.json
│ ├── blog_detail_schema.json
│ ├── user_info_schema.json
│ └── author_info_schema.json
│
├── data/ # YAML测试数据
│ ├── login.yaml
│ ├── get_list.yaml
│ ├── add_blog.yaml
│ ├── get_blog_detail.yaml
│ ├── get_user_info.yaml
│ └── get_author_info.yaml
│
├── utils/ # 工具模块
│ ├── __init__.py
│ ├── logger.py # 日志初始化(三个文件,按大小分割)
│ ├── request_handler.py # 封装requests(自动记录请求/响应日志)
│ ├── yaml_reader.py # 读取YAML文件
│ └── assertions.py # 公共断言:状态码、json schema、字段值
│
├── api/ # 接口层封装
│ ├── __init__.py
│ ├── login_api.py # 登录接口
│ ├── blog_api.py # 博客接口(列表、新增、详情、作者信息)
│ └── user_api.py # 用户接口
│
└── test_case/ # 测试用例(数据驱动)
├── __init__.py
├── test_login.py # 登录测试(8条)
├── test_blog_list.py # 博客列表测试(4条)
├── test_add_blog.py # 新增博客测试(6条)
├── test_blog_detail.py # 博客详情测试(5条)
├── test_user_info.py # 用户信息测试(3条)
└── test_author_info.py # 作者信息测试(5条)
用例覆盖总览(总计 31 条):
| 脚本文件 | 覆盖用例数 | 对应TC编号 |
|---|---|---|
test_login.py |
8条 | TC-LOGIN-01 ~ 08 |
test_blog_list.py |
4条 | TC-LIST-01~04 |
test_blog_detail.py |
5条 | TC-DETAIL-01~05 |
test_add_blog.py |
6条 | TC-ADD-01~06 |
test_user_info.py |
3条 | TC-USER-01~03 |
test_author_info.py |
5条 | TC-AUTHOR-01~05 |
3.3 配置与常量(config/settings.py)
python
"""
项目配置与常量定义
该文件定义了整个测试项目所需的基础配置,包括:
- 接口基础URL
- 登录账号密码
- 请求超时时间
- 日志级别
所有配置项都集中管理,便于维护和修改。
"""
# 接口基础URL
BASE_URL = "http://49.235.61.184:19090"
# 登录固定账号密码
USERNAME = "zhangsan"
PASSWORD = "123456"
# 请求超时时间(秒)
TIMEOUT = 10
# 日志级别:DEBUG, INFO, WARNING, ERROR, CRITICAL
LOG_LEVEL = "DEBUG"
配置说明:
| 配置项 | 值 | 说明 |
|---|---|---|
| BASE_URL | http://49.235.61.184:19090 | API服务基础地址 |
| USERNAME | zhangsan | 测试用登录账号 |
| PASSWORD | 123456 | 测试用登录密码 |
| TIMEOUT | 10 | 请求超时时间(秒) |
| LOG_LEVEL | DEBUG | 日志输出级别 |
3.4 日志配置(utils/logger.py)
实现分级日志输出,三个日志文件,按大小自动分割:
python
"""
日志模块
使用Python标准logging模块创建日志系统,包含三个日志文件:
- all.log: 所有级别日志(DEBUG及以上)
- info.log: INFO级别及以上日志
- error.log: ERROR级别及以上日志
采用RotatingFileHandler实现按文件大小分割,单个文件最大10MB,保留3个备份。
同时日志也会输出到控制台,方便调试。
"""
import logging
import os
from logging.handlers import RotatingFileHandler
from config.settings import LOG_LEVEL
def get_logger(name):
"""
获取日志实例
Args:
name: 日志名称,通常使用__name__
Returns:
logger实例
"""
logger = logging.getLogger(name)
logger.setLevel(logging.DEBUG)
if logger.handlers:
return logger
log_dir = "logs"
if not os.path.exists(log_dir):
os.makedirs(log_dir)
logger.debug(f"【日志目录创建】创建日志目录: {log_dir}")
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# ALL级别日志处理器(记录所有级别日志)
all_handler = RotatingFileHandler(
os.path.join(log_dir, 'all.log'),
maxBytes=10 * 1024 * 1024,
backupCount=3,
encoding='utf-8'
)
all_handler.setLevel(logging.DEBUG)
all_handler.setFormatter(formatter)
# INFO级别日志处理器
info_handler = RotatingFileHandler(
os.path.join(log_dir, 'info.log'),
maxBytes=10 * 1024 * 1024,
backupCount=3,
encoding='utf-8'
)
info_handler.setLevel(logging.INFO)
info_handler.setFormatter(formatter)
# ERROR级别日志处理器
error_handler = RotatingFileHandler(
os.path.join(log_dir, 'error.log'),
maxBytes=10 * 1024 * 1024,
backupCount=3,
encoding='utf-8'
)
error_handler.setLevel(logging.ERROR)
error_handler.setFormatter(formatter)
# 控制台输出处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(LOG_LEVEL)
console_handler.setFormatter(formatter)
logger.addHandler(all_handler)
logger.addHandler(info_handler)
logger.addHandler(error_handler)
logger.addHandler(console_handler)
logger.info(f"【日志系统初始化完成】模块: {name},日志级别: {LOG_LEVEL}")
return logger
日志输出说明:
| 文件 | 日志级别 | 说明 |
|---|---|---|
logs/all.log |
DEBUG、INFO、WARNING、ERROR | 记录所有日志 |
logs/info.log |
INFO、WARNING、ERROR | 记录INFO及以上级别 |
logs/error.log |
ERROR | 记录ERROR级别 |
| 控制台 | 由LOG_LEVEL配置 | 默认DEBUG级别 |
日志特性:
- 每个文件最大 10MB,超过自动分割
- 最多保留 3 个备份文件
- 格式包含:时间戳、模块名、日志级别、消息
- 同时输出到控制台,便于实时调试
3.5 请求封装(utils/request_handler.py)
封装requests库的get和post方法,实现自动日志记录:
python
"""
请求封装模块
封装requests库的get和post方法,实现:
- 自动记录请求信息(方法、URL、请求头、参数)
- 自动记录响应信息(状态码、响应体)
- 异常时记录堆栈信息
- 返回requests.Response对象
该模块是所有API调用的基础,确保日志的完整性和可追溯性。
"""
import requests
import json as json_module
import traceback
from utils.logger import get_logger
from config.settings import TIMEOUT, BASE_URL
logger = get_logger(__name__)
def get(url, params=None, headers=None, **kwargs):
"""封装GET请求"""
full_url = f"{BASE_URL}{url}"
logger.debug(f"【GET请求开始】准备发送请求到: {full_url}")
logger.info(f"【GET请求】URL: {full_url}")
if headers:
logger.info(f"【请求头】{json_module.dumps(headers, ensure_ascii=False)}")
if params:
logger.info(f"【请求参数】{json_module.dumps(params, ensure_ascii=False)}")
logger.debug(f"【请求超时】{TIMEOUT}秒")
try:
response = requests.get(
full_url,
params=params,
headers=headers,
timeout=TIMEOUT,
**kwargs
)
logger.info(f"【响应状态码】{response.status_code}")
try:
response_json = response.json()
logger.info(f"【响应体】{json_module.dumps(response_json, ensure_ascii=False, indent=2)}")
logger.debug(f"【响应耗时】{response.elapsed.total_seconds():.3f}秒")
except ValueError:
logger.warning(f"【响应解析警告】响应体不是JSON格式")
logger.info(f"【响应体】{response.text}")
return response
except requests.exceptions.Timeout:
logger.error(f"【请求超时】URL: {full_url},超时时间: {TIMEOUT}秒")
logger.error(f"【异常堆栈】{traceback.format_exc()}")
raise
except requests.exceptions.ConnectionError:
logger.error(f"【连接错误】URL: {full_url},无法连接到服务器")
logger.error(f"【异常堆栈】{traceback.format_exc()}")
raise
except Exception as e:
logger.error(f"【请求异常】URL: {full_url},错误信息: {str(e)}")
logger.error(f"【异常堆栈】{traceback.format_exc()}")
raise
def post(url, data=None, json_data=None, headers=None, **kwargs):
"""封装POST请求"""
full_url = f"{BASE_URL}{url}"
logger.debug(f"【POST请求开始】准备发送请求到: {full_url}")
logger.info(f"【POST请求】URL: {full_url}")
if headers:
logger.info(f"【请求头】{json_module.dumps(headers, ensure_ascii=False)}")
if data:
logger.info(f"【form-data参数】{json_module.dumps(data, ensure_ascii=False)}")
if json_data:
logger.info(f"【JSON请求体】{json_module.dumps(json_data, ensure_ascii=False, indent=2)}")
logger.debug(f"【请求超时】{TIMEOUT}秒")
try:
response = requests.post(
full_url,
data=data,
json=json_data,
headers=headers,
timeout=TIMEOUT,
**kwargs
)
logger.info(f"【响应状态码】{response.status_code}")
try:
response_json = response.json()
logger.info(f"【响应体】{json_module.dumps(response_json, ensure_ascii=False, indent=2)}")
logger.debug(f"【响应耗时】{response.elapsed.total_seconds():.3f}秒")
except ValueError:
logger.warning(f"【响应解析警告】响应体不是JSON格式")
logger.info(f"【响应体】{response.text}")
return response
except requests.exceptions.Timeout:
logger.error(f"【请求超时】URL: {full_url},超时时间: {TIMEOUT}秒")
logger.error(f"【异常堆栈】{traceback.format_exc()}")
raise
except requests.exceptions.ConnectionError:
logger.error(f"【连接错误】URL: {full_url},无法连接到服务器")
logger.error(f"【异常堆栈】{traceback.format_exc()}")
raise
except Exception as e:
logger.error(f"【请求异常】URL: {full_url},错误信息: {str(e)}")
logger.error(f"【异常堆栈】{traceback.format_exc()}")
raise
请求封装特性:
- 自动拼接BASE_URL,无需每次手动拼接
- 自动记录请求和响应的详细信息
- 异常分类处理(超时、连接错误、通用异常)
- 响应耗时记录便于性能分析
3.6 YAML数据读取(utils/yaml_reader.py)
python
"""
YAML数据读取模块
提供加载YAML文件的功能,约定YAML文件格式:
- 顶层为列表
- 列表元素为字典,包含以下字段:
- case_name: 用例名称
- input: 输入参数(字典)
- expected: 预期结果(字典),至少包含status_code和response_body关键字段
该模块用于测试数据驱动,从YAML文件中读取测试用例数据。
"""
import yaml
import os
from utils.logger import get_logger
logger = get_logger(__name__)
def load_yaml(file_path):
"""
加载YAML文件
Args:
file_path: YAML文件路径(相对路径)
Returns:
Python对象(字典或列表)
"""
abs_path = os.path.abspath(file_path)
logger.debug(f"【YAML加载】尝试加载文件: {abs_path}")
if not os.path.exists(abs_path):
logger.error(f"【YAML加载失败】文件不存在: {abs_path}")
raise FileNotFoundError(f"YAML文件不存在: {abs_path}")
try:
with open(abs_path, 'r', encoding='utf-8') as f:
data = yaml.safe_load(f)
if not isinstance(data, list):
logger.warning(f"【YAML格式警告】文件 {abs_path} 的内容不是列表格式")
logger.info(f"【YAML加载成功】文件: {abs_path},数据条数: {len(data) if isinstance(data, list) else 1}")
return data
except yaml.YAMLError as e:
logger.error(f"【YAML解析错误】文件: {abs_path},错误信息: {str(e)}")
raise
except Exception as e:
logger.error(f"【YAML加载异常】文件: {abs_path},错误信息: {str(e)}")
raise
3.7 公共断言(utils/assertions.py)
python
"""
公共断言模块
提供通用的断言方法,用于测试用例中的验证操作:
- assert_status_code: 校验HTTP状态码
- assert_json_schema: 使用jsonschema校验响应结构
- assert_json_field: 校验特定字段值
- assert_data_not_none: 校验data字段不为空
所有断言失败时会记录ERROR级别日志,并抛出AssertionError。
"""
import json
import os
import traceback
from jsonschema import validate, ValidationError
from utils.logger import get_logger
logger = get_logger(__name__)
def assert_status_code(response, expected_code):
"""校验HTTP状态码"""
actual_code = response.status_code
logger.debug(f"【状态码校验】预期: {expected_code}, 实际: {actual_code}")
if actual_code != expected_code:
logger.error(f"【状态码断言失败】预期: {expected_code}, 实际: {actual_code}")
raise AssertionError(f"状态码不匹配,预期: {expected_code}, 实际: {actual_code}")
logger.info(f"【状态码校验通过】状态码: {actual_code}")
def assert_json_schema(response_json, schema_file_path):
"""使用jsonschema校验响应结构"""
abs_path = os.path.abspath(schema_file_path)
logger.debug(f"【Schema校验】Schema文件: {abs_path}")
if not os.path.exists(abs_path):
logger.error(f"【Schema校验失败】Schema文件不存在: {abs_path}")
raise FileNotFoundError(f"Schema文件不存在: {abs_path}")
try:
with open(abs_path, 'r', encoding='utf-8') as f:
schema = json.load(f)
validate(instance=response_json, schema=schema)
logger.info(f"【Schema校验通过】Schema文件: {abs_path}")
except ValidationError as e:
logger.error(f"【Schema校验失败】错误路径: {e.json_path}, 错误信息: {e.message}")
logger.error(f"【响应JSON】{json.dumps(response_json, ensure_ascii=False, indent=2)}")
raise
except Exception as e:
logger.error(f"【Schema校验异常】错误信息: {str(e)}")
logger.error(f"【异常堆栈】{traceback.format_exc()}")
raise
def assert_json_field(response_json, field, expected_value):
"""校验JSON响应中特定字段的值"""
logger.debug(f"【字段校验】字段: {field}, 预期值: {expected_value}")
fields = field.split('.')
value = response_json
try:
for f in fields:
value = value[f]
except (KeyError, TypeError):
logger.error(f"【字段断言失败】字段不存在: {field}")
raise AssertionError(f"字段不存在: {field}")
if isinstance(value, str) and isinstance(expected_value, str):
value_stripped = value.strip()
expected_stripped = expected_value.strip()
if value_stripped != expected_stripped:
logger.error(f"【字段断言失败】字段: {field}")
logger.error(f" 预期值: {repr(expected_value)} (长度: {len(expected_value)})")
logger.error(f" 实际值: {repr(value)} (长度: {len(value)})")
raise AssertionError(f"字段值不匹配,字段: {field}, 预期: {expected_value}, 实际: {value}")
else:
if value != expected_value:
logger.error(f"【字段断言失败】字段: {field}, 预期: {expected_value}, 实际: {value}")
raise AssertionError(f"字段值不匹配,字段: {field}, 预期: {expected_value}, 实际: {value}")
logger.info(f"【字段校验通过】字段: {field}, 值: {value}")
def assert_data_not_none(response_json):
"""校验data字段不为空"""
data = response_json.get('data')
logger.debug(f"【data校验】data值: {data}")
if data is None:
logger.error(f"【data断言失败】data字段为空")
raise AssertionError("data字段为空")
logger.info(f"【data校验通过】data字段不为空")
3.8 接口层封装(api/目录)
3.8.1 登录接口(api/login_api.py)
python
"""
登录接口封装
提供用户登录功能,使用form-data格式发送POST请求。
"""
from utils.request_handler import post
def login(username, password):
"""
用户登录
Args:
username: 用户名
password: 密码
Returns:
requests.Response对象
"""
url = "/user/login"
data = {
"username": username,
"password": password
}
return post(url, data=data)
3.8.2 博客接口(api/blog_api.py)
python
"""
博客接口封装
提供博客相关的API调用:
- get_list: 获取博客列表
- add_blog: 新增博客
- get_detail: 获取博客详情
- get_author_info: 获取作者信息
"""
from utils.request_handler import get, post
def get_list(headers):
"""获取博客列表"""
url = "/blog/getList"
return get(url, headers=headers)
def add_blog(headers, title, content):
"""新增博客"""
url = "/blog/add"
data = {
"title": title,
"content": content
}
return post(url, json_data=data, headers=headers)
def get_detail(headers, blog_id):
"""获取博客详情"""
url = "/blog/getBlogDetail"
params = {"blogId": blog_id}
return get(url, params=params, headers=headers)
def get_author_info(headers, blog_id):
"""获取作者信息"""
url = "/user/getAuthorInfo"
params = {"blogId": blog_id}
return get(url, params=params, headers=headers)
3.8.3 用户接口(api/user_api.py)
python
"""
用户接口封装
提供用户信息相关的API调用。
"""
from utils.request_handler import get
def get_user_info(headers):
"""获取用户信息"""
url = "/user/getUserInfo"
return get(url, headers=headers)
3.9 全局 Fixture(conftest.py)
提供session级别的登录Token和博客ID:
python
"""
全局Fixture定义
定义pytest的session级别fixture:
- valid_token: 获取有效登录token,用于后续需要认证的接口测试
- blog_id: 获取一个有效的博客ID,用于需要博客ID的接口测试
fixture的作用域为session级别,表示在整个测试会话中只执行一次,
所有测试用例共享同一个token和blog_id,提高测试效率。
"""
import pytest
from api.login_api import login
from api.blog_api import get_list
from config.settings import USERNAME, PASSWORD
from utils.logger import get_logger
logger = get_logger(__name__)
@pytest.fixture(scope="session")
def valid_token():
"""
获取有效登录token(session级别)
Returns:
str: JWT token字符串
"""
logger.info("【Fixture】开始获取登录Token")
response = login(USERNAME, PASSWORD)
result = response.json()
code = result.get("code")
data = result.get("data")
if code != "SUCCESS" or data is None:
error_msg = result.get("errMsg", "登录失败")
logger.error(f"【Fixture失败】获取登录Token失败: {error_msg}")
raise Exception(f"登录失败: {error_msg}")
logger.info(f"【Fixture成功】获取登录Token成功")
return data
@pytest.fixture(scope="session")
def blog_id(valid_token):
"""
获取有效的博客ID(session级别)
Args:
valid_token: 登录token
Returns:
int: 博客ID
"""
logger.info("【Fixture】开始获取博客ID")
headers = {"user_token_header": valid_token}
response = get_list(headers)
result = response.json()
data = result.get("data", [])
if not data:
logger.warning("【Fixture警告】博客列表为空,将跳过相关测试")
pytest.skip("博客列表为空,跳过测试")
blog_id = data[0]["id"]
logger.info(f"【Fixture成功】获取博客ID成功: {blog_id}")
return blog_id
Fixture 说明:
| Fixture | Scope | 说明 |
|---|---|---|
valid_token |
session | 登录Token,整个测试会话只获取一次 |
blog_id |
session | 博客ID,依赖valid_token,只获取一次 |
3.10 JSON Schema 定义(schema/目录)
3.10.1 登录响应 Schema(schema/login_schema.json)
json
{
"type": "object",
"properties": {
"code": {"type": "string", "enum": ["SUCCESS"]},
"errMsg": {"type": "string"},
"data": {"type": "string", "minLength": 1}
},
"required": ["code", "errMsg", "data"]
}
3.10.2 博客列表响应 Schema(schema/blog_list_schema.json)
json
{
"type": "object",
"properties": {
"code": {"type": "string", "enum": ["SUCCESS"]},
"errMsg": {"type": "string"},
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {"type": "integer"},
"title": {"type": "string"},
"content": {"type": "string"},
"createTime": {"type": "string"}
},
"required": ["id", "title", "content", "createTime"]
}
}
},
"required": ["code", "errMsg", "data"]
}
3.10.3 博客详情响应 Schema(schema/blog_detail_schema.json)
json
{
"type": "object",
"properties": {
"code": {"type": "string", "enum": ["SUCCESS"]},
"errMsg": {"type": "string"},
"data": {
"type": "object",
"properties": {
"id": {"type": "integer"},
"title": {"type": "string"},
"content": {"type": "string"},
"createTime": {"type": "string"},
"userId": {"type": "integer"},
"username": {"type": "string"}
},
"required": ["id", "title", "content", "createTime", "userId", "username"]
}
},
"required": ["code", "errMsg", "data"]
}
3.10.4 新增博客响应 Schema(schema/add_blog_schema.json)
json
{
"type": "object",
"properties": {
"code": {"type": "string", "enum": ["SUCCESS"]},
"errMsg": {"type": "string"},
"data": {
"type": "object",
"properties": {
"id": {"type": "integer"},
"title": {"type": "string"},
"content": {"type": "string"}
},
"required": ["id", "title", "content"]
}
},
"required": ["code", "errMsg", "data"]
}
3.10.5 用户信息响应 Schema(schema/user_info_schema.json)
json
{
"type": "object",
"properties": {
"code": {"type": "string", "enum": ["SUCCESS"]},
"errMsg": {"type": "string"},
"data": {
"type": "object",
"properties": {
"id": {"type": "integer"},
"username": {"type": "string"},
"password": {"type": "string"}
},
"required": ["id", "username", "password"]
}
},
"required": ["code", "errMsg", "data"]
}
3.10.6 作者信息响应 Schema(schema/author_info_schema.json)
json
{
"type": "object",
"properties": {
"code": {"type": "string", "enum": ["SUCCESS"]},
"errMsg": {"type": "string"},
"data": {
"type": "object",
"properties": {
"id": {"type": "integer"},
"username": {"type": "string"}
},
"required": ["id", "username"]
}
},
"required": ["code", "errMsg", "data"]
}
3.11 YAML测试数据示例
3.11.1 登录测试数据(data/login.yaml)
yaml
- case_name: "正常登录"
input:
username: "zhangsan"
password: "123456"
expected:
status_code: 200
code: "SUCCESS"
errMsg: ""
data_not_null: true
- case_name: "用户名为空"
input:
username: ""
password: "123456"
expected:
status_code: 200
code: "FAIL"
errMsg: "账号或密码不能为空"
data_null: true
- case_name: "密码为空"
input:
username: "zhangsan"
password: ""
expected:
status_code: 200
code: "FAIL"
errMsg: "账号或密码不能为空"
data_null: true
- case_name: "用户名密码均空"
input:
username: ""
password: ""
expected:
status_code: 200
code: "FAIL"
errMsg: "账号或密码不能为空"
data_null: true
- case_name: "用户名错误"
input:
username: "wrong"
password: "123456"
expected:
status_code: 200
code: "FAIL"
errMsg: "用户不存在"
data_null: true
- case_name: "密码错误"
input:
username: "zhangsan"
password: "wrong"
expected:
status_code: 200
code: "FAIL"
errMsg: "密码错误"
data_null: true
- case_name: "超长用户名"
input:
username: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
password: "123456"
expected:
status_code: 200
code: "FAIL"
errMsg: "用户不存在"
data_null: true
- case_name: "SQL注入攻击"
input:
username: "'OR 1=1--"
password: "123456"
expected:
status_code: 200
code: "FAIL"
errMsg: "用户不存在"
data_null: true
3.11.2 博客列表测试数据(data/get_list.yaml)
yaml
- case_name: "正常获取列表"
input:
token_required: true
expected:
status_code: 200
code: "SUCCESS"
errMsg: ""
data_not_null: true
- case_name: "Token缺失"
input:
token_required: false
expected:
status_code: 401
- case_name: "Token格式错误"
input:
token_required: false
custom_token: "invalid_token_123"
expected:
status_code: 401
- case_name: "Token过期"
input:
token_required: false
custom_token: "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6NSwidXNlck5hbWUiOiJ6aGFuZ3NhbiIsImV4cCI6MTcwNDMyMDg1MH0.tRfYNXD0p8zU4-orOMaLunkkESHgLp7mJshNlgJETSM"
expected:
status_code: 401
3.11.3 新增博客测试数据(data/add_blog.yaml)
yaml
- case_name: "正常新增博客"
input:
token_required: true
title: "测试标题"
content: "测试内容"
expected:
status_code: 200
code: "SUCCESS"
errMsg: ""
data: true
- case_name: "Token缺失"
input:
token_required: false
title: "测试标题"
content: "测试内容"
expected:
status_code: 401
- case_name: "标题缺失"
input:
token_required: true
title: ""
content: "测试内容"
expected:
status_code: 200
code: "SUCCESS"
errMsg: ""
data: false
- case_name: "内容缺失"
input:
token_required: true
title: "测试标题"
content: ""
expected:
status_code: 200
code: "SUCCESS"
errMsg: ""
data: false
- case_name: "超长标题"
input:
token_required: true
title: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
content: "测试内容"
expected:
status_code: 200
code: "SUCCESS"
errMsg: ""
data: false
- case_name: "XSS攻击内容"
input:
token_required: true
title: "测试标题"
content: "<script>alert(1)</script>"
expected:
status_code: 200
code: "SUCCESS"
errMsg: ""
data: true
3.11.4 博客详情测试数据(data/get_blog_detail.yaml)
yaml
- case_name: "正常获取详情"
input:
token_required: true
blog_id_required: true
expected:
status_code: 200
code: "SUCCESS"
errMsg: ""
data_not_null: true
- case_name: "Token缺失"
input:
token_required: false
blog_id_required: true
expected:
status_code: 401
- case_name: "blogId参数缺失"
input:
token_required: true
blog_id_required: false
expected:
status_code: 200
code: "FAIL"
errMsg: "内部错误, 请联系管理员"
data_null: true
- case_name: "非法blogId格式"
input:
token_required: true
blog_id_required: false
custom_blog_id: "abc"
expected:
status_code: 200
code: "FAIL"
errMsg: "内部错误, 请联系管理员"
data_null: true
- case_name: "不存在的blogId"
input:
token_required: true
blog_id_required: false
custom_blog_id: 99999
expected:
status_code: 200
code: "FAIL"
errMsg: "内部错误, 请联系管理员"
data_null: true
3.11.5 用户信息测试数据(data/get_user_info.yaml)
yaml
- case_name: "正常获取信息"
input:
token_required: true
expected:
status_code: 200
code: "SUCCESS"
errMsg: ""
data_not_null: true
- case_name: "Token缺失"
input:
token_required: false
expected:
status_code: 401
- case_name: "Token篡改"
input:
token_required: false
custom_token: "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6NSwidXNlck5hbWUiOiJ6aGFuZ3NhbiIsImV4cCI6MTcwNDMyMDg1MH0.tRfYNXD0p8zU4-orOMaLunkkESHgLp7mJshNlgJETSX"
expected:
status_code: 401
3.11.6 作者信息测试数据(data/get_author_info.yaml)
yaml
- case_name: "正常获取作者信息"
input:
token_required: true
blog_id_required: true
expected:
status_code: 200
code: "SUCCESS"
errMsg: ""
data_not_null: true
- case_name: "blogId参数缺失"
input:
token_required: true
blog_id_required: false
expected:
status_code: 200
code: "FAIL"
errMsg: "内部错误, 请联系管理员"
data_null: true
- case_name: "非法blogId格式"
input:
token_required: true
blog_id_required: false
custom_blog_id: "abc"
expected:
status_code: 200
code: "FAIL"
errMsg: "内部错误, 请联系管理员"
data_null: true
- case_name: "不存在的blogId"
input:
token_required: true
blog_id_required: false
custom_blog_id: 99999
expected:
status_code: 200
code: "FAIL"
errMsg: "内部错误, 请联系管理员"
data_null: true
- case_name: "Token缺失"
input:
token_required: false
blog_id_required: true
expected:
status_code: 401
3.12 测试用例实现(test_case/目录)
3.12.1 登录测试(test_case/test_login.py)
python
"""
用户登录模块测试用例
使用@pytest.mark.parametrize从YAML文件加载测试数据,实现数据驱动测试。
每个测试用例对应一个登录场景,覆盖正常登录和各种异常情况。
"""
import pytest
import allure
from utils.yaml_reader import load_yaml
from utils.assertions import assert_status_code, assert_json_schema, assert_json_field
from api.login_api import login
# 从YAML文件加载测试数据
data = load_yaml("data/login.yaml")
@allure.feature("用户登录模块")
class TestLogin:
"""登录接口测试类"""
@pytest.mark.parametrize("case", data, ids=lambda x: x["case_name"])
def test_login(self, case):
"""
登录测试用例
使用parametrize参数化,从YAML文件读取测试数据:
- case_name: 用例名称,用于标识每个测试用例
- input: 输入参数(username, password)
- expected: 预期结果(status_code, code, errMsg等)
执行步骤:
1. 获取输入参数和预期结果
2. 调用登录接口
3. 校验HTTP状态码
4. 解析响应JSON
5. 成功场景下校验JSON Schema
6. 校验code和errMsg字段值
7. 根据预期校验data字段是否为空
"""
with allure.step(f"执行用例:{case['case_name']}"):
# 获取输入参数
input_data = case["input"]
username = input_data["username"]
password = input_data["password"]
# 获取预期结果
expected = case["expected"]
# 调用登录接口
response = login(username, password)
# 校验HTTP状态码
assert_status_code(response, expected["status_code"])
# 解析响应JSON
result = response.json()
# 只有成功场景才校验schema
if expected.get("code") == "SUCCESS":
assert_json_schema(result, "schema/login_schema.json")
# 校验code字段
if "code" in expected:
assert_json_field(result, "code", expected["code"])
# 校验errMsg字段
if "errMsg" in expected:
assert_json_field(result, "errMsg", expected["errMsg"])
# 校验data字段是否为空
if expected.get("data_not_null"):
assert result["data"] is not None, "data字段应为非空"
if expected.get("data_null"):
assert result["data"] is None, "data字段应为空"
3.12.2 博客列表测试(test_case/test_blog_list.py)
python
"""
获取博客列表模块测试用例
测试获取博客列表接口,覆盖正常获取和各种异常场景。
依赖valid_token fixture获取登录token。
"""
import pytest
import allure
from utils.yaml_reader import load_yaml
from utils.assertions import assert_status_code, assert_json_schema, assert_json_field
from api.blog_api import get_list
# 从YAML文件加载测试数据
data = load_yaml("data/get_list.yaml")
@allure.feature("博客列表模块")
class TestBlogList:
"""获取博客列表接口测试类"""
@pytest.mark.parametrize("case", data, ids=lambda x: x["case_name"])
def test_get_list(self, case, valid_token):
"""
获取博客列表测试用例
参数化测试,覆盖多种场景:
- 正常获取列表(需要有效token)
- Token缺失(401错误)
- Token格式错误(401错误)
- Token过期(401错误)
动态构造请求头:根据测试数据决定是否使用有效token
"""
with allure.step(f"执行用例:{case['case_name']}"):
# 获取输入参数和预期结果
input_data = case["input"]
expected = case["expected"]
# 构造请求头
headers = {}
if input_data.get("token_required"):
headers["user_token_header"] = valid_token
elif input_data.get("custom_token"):
headers["user_token_header"] = input_data["custom_token"]
# 调用获取博客列表接口
response = get_list(headers)
# 校验HTTP状态码
assert_status_code(response, expected["status_code"])
# 如果状态码不是401,继续校验响应内容
if expected["status_code"] != 401:
result = response.json()
# 成功场景校验schema
if expected.get("code") == "SUCCESS":
assert_json_schema(result, "schema/blog_list_schema.json")
# 校验code字段
if "code" in expected:
assert_json_field(result, "code", expected["code"])
# 校验errMsg字段
if "errMsg" in expected:
assert_json_field(result, "errMsg", expected["errMsg"])
# 校验data字段
if expected.get("data_not_null"):
assert result["data"] is not None, "data字段应为非空"
3.12.3 新增博客测试(test_case/test_add_blog.py)
python
"""
新增博客模块测试用例
测试新增博客接口,覆盖正常新增和各种异常场景。
依赖valid_token fixture获取登录token。
"""
import pytest
import allure
from utils.yaml_reader import load_yaml
from utils.assertions import assert_status_code, assert_json_schema, assert_json_field
from api.blog_api import add_blog
# 从YAML文件加载测试数据
data = load_yaml("data/add_blog.yaml")
@allure.feature("新增博客模块")
class TestAddBlog:
"""新增博客接口测试类"""
@pytest.mark.parametrize("case", data, ids=lambda x: x["case_name"])
def test_add_blog(self, case, valid_token):
"""
新增博客测试用例
参数化测试,覆盖多种场景:
- 正常新增博客(有效token+合规参数)
- Token缺失(401错误)
- 标题缺失(返回false)
- 内容缺失(返回false)
- 超长标题(返回false)
- XSS攻击内容(应成功,说明系统有XSS防护)
动态构造请求头和请求参数
"""
with allure.step(f"执行用例:{case['case_name']}"):
# 获取输入参数和预期结果
input_data = case["input"]
expected = case["expected"]
# 构造请求头
headers = {}
if input_data.get("token_required"):
headers["user_token_header"] = valid_token
# 获取博客标题和内容
title = input_data["title"]
content = input_data["content"]
# 调用新增博客接口
response = add_blog(headers, title, content)
# 校验HTTP状态码
assert_status_code(response, expected["status_code"])
# 如果状态码不是401,继续校验响应内容
if expected["status_code"] != 401:
result = response.json()
# 成功场景校验schema
if expected.get("code") == "SUCCESS":
assert_json_schema(result, "schema/add_blog_schema.json")
# 校验code字段
if "code" in expected:
assert_json_field(result, "code", expected["code"])
# 校验errMsg字段
if "errMsg" in expected:
assert_json_field(result, "errMsg", expected["errMsg"])
# 校验data字段值
if "data" in expected:
assert_json_field(result, "data", expected["data"])
3.12.4 博客详情测试(test_case/test_blog_detail.py)
python
"""
获取博客详情模块测试用例
测试获取博客详情接口,覆盖正常获取和各种异常场景。
依赖valid_token fixture获取登录token,依赖blog_id fixture获取有效博客ID。
"""
import pytest
import allure
from utils.yaml_reader import load_yaml
from utils.assertions import assert_status_code, assert_json_schema, assert_json_field
from api.blog_api import get_detail
# 从YAML文件加载测试数据
data = load_yaml("data/get_blog_detail.yaml")
@allure.feature("博客详情模块")
class TestBlogDetail:
"""获取博客详情接口测试类"""
@pytest.mark.parametrize("case", data, ids=lambda x: x["case_name"])
def test_get_blog_detail(self, case, valid_token, blog_id):
"""
获取博客详情测试用例
参数化测试,覆盖多种场景:
- 正常获取详情(有效token+有效blogId)
- Token缺失(401错误)
- blogId参数缺失(返回错误)
- 非法blogId格式(返回错误)
- 不存在的blogId(返回错误)
动态构造请求头和查询参数
"""
with allure.step(f"执行用例:{case['case_name']}"):
# 获取输入参数和预期结果
input_data = case["input"]
expected = case["expected"]
# 构造请求头
headers = {}
if input_data.get("token_required"):
headers["user_token_header"] = valid_token
# 确定博客ID
if input_data.get("blog_id_required"):
current_blog_id = blog_id
elif input_data.get("custom_blog_id"):
current_blog_id = input_data["custom_blog_id"]
else:
current_blog_id = None
# 调用获取博客详情接口
response = get_detail(headers, current_blog_id)
# 校验HTTP状态码
assert_status_code(response, expected["status_code"])
# 如果状态码不是401,继续校验响应内容
if expected["status_code"] != 401:
result = response.json()
# 成功场景校验schema
if expected.get("code") == "SUCCESS":
assert_json_schema(result, "schema/blog_detail_schema.json")
# 校验code字段
if "code" in expected:
assert_json_field(result, "code", expected["code"])
# 校验errMsg字段
if "errMsg" in expected:
assert_json_field(result, "errMsg", expected["errMsg"])
# 校验data字段
if expected.get("data_not_null"):
assert result["data"] is not None, "data字段应为非空"
if expected.get("data_null"):
assert result["data"] is None, "data字段应为空"
3.12.5 用户信息测试(test_case/test_user_info.py)
python
"""
获取用户信息模块测试用例
测试获取当前登录用户信息接口,覆盖正常获取和各种异常场景。
依赖valid_token fixture获取登录token。
"""
import pytest
import allure
from utils.yaml_reader import load_yaml
from utils.assertions import assert_status_code, assert_json_schema, assert_json_field
from api.user_api import get_user_info
# 从YAML文件加载测试数据
data = load_yaml("data/get_user_info.yaml")
@allure.feature("用户信息模块")
class TestUserInfo:
"""获取用户信息接口测试类"""
@pytest.mark.parametrize("case", data, ids=lambda x: x["case_name"])
def test_get_user_info(self, case, valid_token):
"""
获取用户信息测试用例
参数化测试,覆盖多种场景:
- 正常获取信息(有效token)
- Token缺失(401错误)
- Token篡改(401错误)
动态构造请求头:根据测试数据决定是否使用有效token
"""
with allure.step(f"执行用例:{case['case_name']}"):
# 获取输入参数和预期结果
input_data = case["input"]
expected = case["expected"]
# 构造请求头
headers = {}
if input_data.get("token_required"):
headers["user_token_header"] = valid_token
elif input_data.get("custom_token"):
headers["user_token_header"] = input_data["custom_token"]
# 调用获取用户信息接口
response = get_user_info(headers)
# 校验HTTP状态码
assert_status_code(response, expected["status_code"])
# 如果状态码不是401,继续校验响应内容
if expected["status_code"] != 401:
result = response.json()
# 成功场景校验schema
if expected.get("code") == "SUCCESS":
assert_json_schema(result, "schema/user_info_schema.json")
# 校验code字段
if "code" in expected:
assert_json_field(result, "code", expected["code"])
# 校验errMsg字段
if "errMsg" in expected:
assert_json_field(result, "errMsg", expected["errMsg"])
# 校验data字段
if expected.get("data_not_null"):
assert result["data"] is not None, "data字段应为非空"
3.12.6 作者信息测试(test_case/test_author_info.py)
python
"""
获取作者信息模块测试用例
测试获取博客作者信息接口,覆盖正常获取和各种异常场景。
依赖valid_token fixture获取登录token,依赖blog_id fixture获取有效博客ID。
"""
import pytest
import allure
from utils.yaml_reader import load_yaml
from utils.assertions import assert_status_code, assert_json_schema, assert_json_field
from api.blog_api import get_author_info
# 从YAML文件加载测试数据
data = load_yaml("data/get_author_info.yaml")
@allure.feature("作者信息模块")
class TestAuthorInfo:
"""获取作者信息接口测试类"""
@pytest.mark.parametrize("case", data, ids=lambda x: x["case_name"])
def test_get_author_info(self, case, valid_token, blog_id):
"""
获取作者信息测试用例
参数化测试,覆盖多种场景:
- 正常获取作者信息(有效token+有效blogId)
- blogId参数缺失(返回错误)
- 非法blogId格式(返回错误)
- 不存在的blogId(返回错误)
- Token缺失(401错误)
动态构造请求头和查询参数
"""
with allure.step(f"执行用例:{case['case_name']}"):
# 获取输入参数和预期结果
input_data = case["input"]
expected = case["expected"]
# 构造请求头
headers = {}
if input_data.get("token_required"):
headers["user_token_header"] = valid_token
# 确定博客ID
if input_data.get("blog_id_required"):
current_blog_id = blog_id
elif input_data.get("custom_blog_id"):
current_blog_id = input_data["custom_blog_id"]
else:
current_blog_id = None
# 调用获取作者信息接口
response = get_author_info(headers, current_blog_id)
# 校验HTTP状态码
assert_status_code(response, expected["status_code"])
# 如果状态码不是401,继续校验响应内容
if expected["status_code"] != 401:
result = response.json()
# 成功场景校验schema
if expected.get("code") == "SUCCESS":
assert_json_schema(result, "schema/author_info_schema.json")
# 校验code字段
if "code" in expected:
assert_json_field(result, "code", expected["code"])
# 校验errMsg字段
if "errMsg" in expected:
assert_json_field(result, "errMsg", expected["errMsg"])
# 校验data字段
if expected.get("data_not_null"):
assert result["data"] is not None, "data字段应为非空"
if expected.get("data_null"):
assert result["data"] is None, "data字段应为空"
3.13 pytest.ini 配置
ini
[pytest]
# 命令行参数:-s显示输出,-v详细模式
addopts = -sv
# 测试用例目录
testpaths =
test_case
# 测试文件匹配模式
python_files =
test_*.py
# 测试类匹配模式
python_classes =
Test*
# 测试方法匹配模式
python_functions =
test_*
# 日志配置
log_cli = true
log_cli_level = DEBUG
log_cli_format = %(asctime)s - %(name)s - %(levelname)s - %(message)s
# 插件配置
plugins =
allure_pytest
pytest_html
3.14 测试执行命令
bash
# 运行全部用例并生成 Allure 原始数据
pytest --alluredir=reports/allure_results
# 生成并打开报告(需已安装 Allure 命令行)
allure generate reports/allure_results -o reports/allure_report --clean
allure open reports/allure_report
# 运行指定测试类
pytest test_case/test_login.py::TestLogin
# 运行指定测试方法
pytest test_case/test_login.py::TestLogin::test_login
# 生成HTML报告
pytest --html=reports/report.html
# 运行单个测试文件
pytest test_case/test_blog_list.py
# 运行多个测试文件
pytest test_case/test_login.py test_case/test_blog_list.py
四、自动化测试优势
| 特性 | 说明 |
|---|---|
| 数据驱动 | 测试数据与代码分离,使用YAML管理,便于维护和扩展 |
| Schema校验 | 使用jsonschema校验响应结构,确保API返回格式正确 |
| 日志分级 | 三个日志文件(all.log/info.log/error.log),按大小自动分割 |
| Token复用 | session级别fixture,整个测试会话只获取一次Token,提高效率 |
| 断言封装 | 公共断言方法,统一校验逻辑,减少重复代码 |
| 易扩展 | 新增接口只需添加API封装、测试数据和测试用例 |
| 报告生成 | 支持Allure和HTML两种报告格式,便于测试结果展示 |
| 错误追踪 | 完整的异常堆栈记录,便于问题定位和排查 |
五、总结
本文介绍了基于 Pytest + Requests + Allure + Logging 的博客系统API自动化测试实践。从手动用例设计到脚本实现,遵循数据驱动、代码复用、易于维护的原则:
api/目录封装所有API接口,统一调用方式,降低耦合度utils/目录提供日志、请求、断言等公共工具,提高代码复用率data/目录使用YAML管理测试数据,实现数据驱动测试schema/目录定义JSON Schema,确保响应结构正确test_case/目录按接口组织测试用例,共31条,覆盖核心业务场景conftest.py提供全局Fixture,共享登录状态和测试数据