基于 Pytest + Requests + Allure 的博客系统API自动化测试实践

一、项目测试背景

本次测试的博客系统后端 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,共享登录状态和测试数据
相关推荐
小卓(friendhan2005)10 小时前
基于 Pytest + Selenium + Allure 的博客系统自动化测试实践
selenium·测试工具·pytest
金玉满堂@bj1 天前
Pytest 完整使用教程
运维·服务器·pytest
测试员周周1 天前
【Appium 系列】第10节-手势操作实战 — 滑动、拖拽、缩放与轻拂
linux·服务器·开发语言·人工智能·python·appium·pytest
金玉满堂@bj1 天前
pytest+uiautomation+allure 数据驱动桌面自动化项目搭建指南-yaml版本
运维·自动化·pytest
金玉满堂@bj1 天前
pytest+uiautomation+allure+Excel 数据驱动桌面自动化
自动化·excel·pytest
Be reborn2 天前
用例不是孤立执行的:依赖、变量池与 storage_state 设计
python·自动化·pytest
小陈的进阶之路2 天前
安集商城接口自动化项目架构介绍
python·自动化·pytest
测试员周周2 天前
【Appium 系列】第08节-pytest 集成 — conftest.py 中的 fixture 与 hook
开发语言·人工智能·python·功能测试·appium·测试用例·pytest
Be reborn2 天前
从一行 CSV 到一次浏览器操作:关键字驱动执行引擎设计
python·自动化·pytest