一、项目介绍

该项目基于Python+pytest+sqlalchemy+requests+allure+jsonpath+yaml+Jenkins+Linux
github链接:点击这里

二、项目结构说明

project-root/                          # 项目根目录
├─ base/                               # 基础类封装(核心功能)
│  ├─ api_request.py                   # 接口请求基类(封装requests)
│  ├─ test_case_tools.py               # 测试用例工具类(如数据处理、断言)
│  └─ driver.py                        # 浏览器驱动封装(可选,UI自动化用)
├─ common/                             # 公共方法封装(可复用工具)
│  ├─ log_utils.py                     # 日志处理工具
│  ├─ excel_parser.py                  # Excel数据解析工具
│  ├─ yaml_parser.py                   # YAML数据解析工具
│  └─ db_operation.py                  # 数据库操作工具(如MySQL)
├─ conf/                               # 全局配置目录
│  ├─ config.ini                       # 环境配置文件(API地址、账号等)
│  ├─ allure_config.yml                # Allure报告自定义配置
│  └─ env.py                           # 环境变量管理文件
├─ data/                               # 测试数据目录
│  ├─ api_data/                        # 接口测试数据
│  │  ├─ login_data.yaml                # 登录接口测试用例数据
│  │  └─ order_data.xlsx                # 订单接口Excel数据
│  └─ ui_data/                          # UI自动化测试数据(可选)
│     └─ page_elements.yaml             # 页面元素定位数据
├─ logs/                               # 测试日志目录(自动生成)
│  └─ test_20231001.log                 # 按日期命名的日志文件
├─ report/                             # 测试报告目录
│  ├─ allure_html/                      # Allure交互式报告(自动生成)
│  ├─ tm_report/                        # TMReport表格报告(自动生成)
│  └─ report_config.py                  # 报告生成配置文件
├─ testcase/                           # 测试用例目录
│  ├─ api_test/                         # 接口测试用例
│  │  ├─ test_login.py                  # 登录接口测试类
│  │  └─ test_order.py                  # 订单接口测试类
│  └─ ui_test/                          # UI自动化测试用例(可选)
│     └─ test_homepage.py               # 首页UI测试类
├─ venv/                               # 虚拟环境目录(自动生成)
├─ conftest.py                         # pytest全局钩子文件(固定名称)
├─ environment.xml                     # Allure报告环境信息文件
├─ extract.yaml                        # 接口依赖参数存储文件
├─ pytest.ini                          # pytest配置文件(固定名称)
├─ requirements.txt                    # 第三方库依赖清单
└─ run.py                              # 主程序入口(执行测试和生成报告)

三、核心代码

1. run.py(程序入口)

import shutil
import pytest
import os
import webbrowser
from conf.setting import REPORT_TYPE
 
if __name__ == '__main__':
 
    if REPORT_TYPE == 'allure':
        pytest.main(
            ['-s', '-v', '--alluredir=./report/temp', './testcase', '--clean-alluredir',
             '--junitxml=./report/results.xml'])
 
        shutil.copy('./environment.xml', './report/temp')
        os.system(f'allure serve ./report/temp')
 
    elif REPORT_TYPE == 'tm':
        pytest.main(['-vs', '--pytest-tmreport-name=testReport.html', '--pytest-tmreport-path=./report/tmreport'])
        webbrowser.open_new_tab(os.getcwd() + '/report/tmreport/testReport.html')
代码说明
主程序入口
if __name__ == '__main__':
Allure 报告处理逻辑
    if REPORT_TYPE == 'allure':
        pytest.main(
            ['-s', '-v', '--alluredir=./report/temp', './testcase', '--clean-alluredir',
             '--junitxml=./report/results.xml'])
参数说明:
参数含义
-s输出所有打印信息(不屏蔽 stdout)
-v显示详细测试结果
–alluredir=./report/temp将 Allure 报告数据保存到指定目录
./testcase指定测试用例所在目录
–clean-alluredir在每次运行前清空之前的报告数据
–junitxml=./report/results.xml生成 JUnit XML 格式的测试结果文件
        shutil.copy('./environment.xml', './report/temp')

复制环境信息文件 environment.xml 到报告目录中,Allure 报告会读取此文件并显示当前测试环境信息。

        os.system(f'allure serve ./report/temp')

使用 Allure CLI 命令启动本地服务器并展示生成的报告页面,会在默认浏览器中自动打开报告。

2. 测试用例 testcase

商务管理代码示例:
import allure
import pytest
 
from common.readyaml import get_testcase_yaml
from base.apiutil_business import RequestBase
from base.generateId import m_id, c_id
 
 
# 注意:业务场景的接口测试要调用base目录下的apiutil_business文件
 
@allure.feature(next(m_id) + '电子商务管理系统(业务场景)')
class TestEBusinessScenario:
 
    @allure.story(next(c_id) + '商品列表到下单支付流程')
    @pytest.mark.parametrize('case_info', get_testcase_yaml('./testcase/Business interface/BusinessScenario.yml'))
    def test_business_scenario(self, case_info):
        allure.dynamic.title(case_info['baseInfo']['api_name'])
        RequestBase().specification_yaml(case_info)
代码说明
@pytest.mark.parametrize('case_info', get_testcase_yaml('./testcase/Business interface/BusinessScenario.yml'))

实现参数化测试,即一个测试方法可以运行多组不同的输入数据,每一组 case_info 数据都会触发一次完整的测试执行。

yaml文件示例
- baseInfo:
    api_name: 获取商品列表
    method: GET
    url: /product/list
- baseInfo:
    api_name: 提交订单
    method: POST
    url: /order/create
测试方法
def test_business_scenario(self, case_info):
  • 这是一个 pytest 测试方法,每个参数化的 case_info 都会触发一次该方法的执行。
  • self 表示这是类中的一个实例方法(属于 TestEBusinessScenario 类)。
  • case_info 是从 YAML 文件中加载的一条测试用例的数据字典。
动态设置 Allure 报告标题
allure.dynamic.title(case_info['baseInfo']['api_name'])

在生成的 Allure 报告中,为当前测试用例设置一个可读性更强的标题。

执行接口请求和校验
RequestBase().specification_yaml(case_info)
作用:
  • 创建 RequestBase 实例,并调用其 specification_yaml() 方法。
  • 该方法接收当前的测试用例数据 case_info,并根据其中的配置(如 URL、方法、请求头、预期结果等)发送 HTTP 请求。
  • 同时会进行响应断言、日志记录等操作,完成整个接口测试流程。

3. 接口测试 specification_yaml() 方法

代码示例
    def specification_yaml(self, base_info, test_case):
        """
        接口请求处理基本方法
        :param base_info: yaml文件里面的baseInfo
        :param test_case: yaml文件里面的testCase
        :return:
        """
        try:
            params_type = ['data', 'json', 'params']
            url_host = self.conf.get_section_for_data('api_envi', 'host')
            api_name = base_info['api_name']
            allure.attach(api_name, f'接口名称:{api_name}', allure.attachment_type.TEXT)
            url = url_host + base_info['url']
            allure.attach(api_name, f'接口地址:{url}', allure.attachment_type.TEXT)
            method = base_info['method']
            allure.attach(api_name, f'请求方法:{method}', allure.attachment_type.TEXT)
            header = self.replace_load(base_info['header'])
            allure.attach(api_name, f'请求头:{header}', allure.attachment_type.TEXT)
            # 处理cookie
            cookie = None
            if base_info.get('cookies') is not None:
                cookie = eval(self.replace_load(base_info['cookies']))
            case_name = test_case.pop('case_name')
            allure.attach(api_name, f'测试用例名称:{case_name}', allure.attachment_type.TEXT)
            # 处理断言
            val = self.replace_load(test_case.get('validation'))
            test_case['validation'] = val
            validation = eval(test_case.pop('validation'))
            # 处理参数提取
            extract = test_case.pop('extract', None)
            extract_list = test_case.pop('extract_list', None)
            # 处理接口的请求参数
            for key, value in test_case.items():
                if key in params_type:
                    test_case[key] = self.replace_load(value)
 
            # 处理文件上传接口
            file, files = test_case.pop('files', None), None
            if file is not None:
                for fk, fv in file.items():
                    allure.attach(json.dumps(file), '导入文件')
                    files = {fk: open(fv, mode='rb')}
 
            res = self.run.run_main(name=api_name, url=url, case_name=case_name, header=header, method=method,
                                    file=files, cookies=cookie, **test_case)
            status_code = res.status_code
            allure.attach(self.allure_attach_response(res.json()), '接口响应信息', allure.attachment_type.TEXT)
 
            try:
                res_json = json.loads(res.text)  # 把json格式转换成字典字典
                if extract is not None:
                    self.extract_data(extract, res.text)
                if extract_list is not None:
                    self.extract_data_list(extract_list, res.text)
                # 处理断言
                self.asserts.assert_result(validation, res_json, status_code)
            except JSONDecodeError as js:
                logs.error('系统异常或接口未请求!')
                raise js
            except Exception as e:
                logs.error(e)
                raise e
 
        except Exception as e:
            raise e
specification_yaml 方法解析

该方法用于处理 YAML 文件中定义的接口测试用例,包括请求参数构造、动态变量替换、接口调用、响应断言以及数据提取等核心功能。

方法定义
def specification_yaml(self, base_info, test_case):
    """
    接口请求处理基本方法
    :param base_info: yaml文件里面的baseInfo(接口基本信息)
    :param test_case: yaml文件里面的testCase(测试用例数据)
    :return: 无返回值,但通过断言验证接口行为
    """
步骤详解
1. 定义常量与基础配置
params_type = ['data', 'json', 'params']
url_host = self.conf.get_section_for_data('api_envi', 'host')
  • params_type:表示请求参数可能包含的类型字段。
  • url_host:从配置文件中获取当前环境的主机地址。
2. 提取接口基本信息并添加 Allure 报告附件
api_name = base_info['api_name']
allure.attach(api_name, f'接口名称:{api_name}', allure.attachment_type.TEXT)
url = url_host + base_info['url']
allure.attach(api_name, f'接口地址:{url}', allure.attachment_type.TEXT)
method = base_info['method']
allure.attach(api_name, f'请求方法:{method}', allure.attachment_type.TEXT)
  • 从 base_info 中提取接口名称、URL 和请求方法。
  • 使用 allure.attach 将这些信息附加到 Allure 报告中,便于测试结果查看。
3. 处理请求头和 Cookie
header = self.replace_load(base_info['header'])
allure.attach(api_name, f'请求头:{header}', allure.attachment_type.TEXT)
 
cookie = None
if base_info.get('cookies') is not None:
    cookie = eval(self.replace_load(base_info['cookies']))
  • 调用 replace_load 对 header 进行变量替换。
  • 如果存在 cookies,则同样进行替换并使用 eval 转换为字典格式。
4. 提取测试用例名称并添加报告附件
case_name = test_case.pop('case_name')
allure.attach(api_name, f'测试用例名称:{case_name}', allure.attachment_type.TEXT)
5. 处理断言逻辑
val = self.replace_load(test_case.get('validation'))
test_case['validation'] = val
validation = eval(test_case.pop('validation'))
  • 替换断言表达式中的变量。
  • 使用 eval 执行断言表达式,生成实际断言规则。
6. 提取数据字段(extract / extract_list)
extract = test_case.pop('extract', None)
extract_list = test_case.pop('extract_list', None)
  • 从 test_case 中提取需要提取的字段名,供后续从响应中提取数据。
7. 处理请求参数(data/json/params)
for key, value in test_case.items():
    if key in params_type:
        test_case[key] = self.replace_load(value)
8. 处理文件上传
file, files = test_case.pop('files', None), None
if file is not None:
    for fk, fv in file.items():
        allure.attach(json.dumps(file), '导入文件')
        files = {fk: open(fv, mode='rb')}
9. 发送接口请求
res = self.run.run_main(name=api_name, url=url, case_name=case_name, header=header,
                        method=method, file=files, cookies=cookie, **test_case)
status_code = res.status_code
allure.attach(self.allure_attach_response(res.json()), '接口响应信息', allure.attachment_type.TEXT)
  • 调用 run_main 方法发送请求。
  • 获取状态码和响应内容。
  • 将响应内容附加到 Allure 报告中。
10. 处理响应与断言
try:
    res_json = json.loads(res.text)  # 把json格式转换成字典
    if extract is not None:
        self.extract_data(extract, res.text)
    if extract_list is not None:
        self.extract_data_list(extract_list, res.text)
    # 处理断言
    self.asserts.assert_result(validation, res_json, status_code)
except JSONDecodeError as js:
    logs.error('系统异常或接口未请求!')
    raise js
except Exception as e:
    logs.error(e)
    raise e
  • 将响应内容转为 JSON 字典。
  • 若有 extract 或 extract_list,则调用对应方法提取数据。
  • 使用断言方法对响应结果进行验证。
11. 异常捕获
except Exception as e:
    raise e
  • 捕获所有异常并重新抛出,确保测试框架可以正确识别失败情况。
示例说明
baseInfo:
  api_name: 登录接口
  url: /login
  method: POST
  header:
    Content-Type: application/json
  cookies: null
 
testCase:
  - case_name: 正常登录
    json:
      username: admin
      password: ${get_password()}
    validation:
      code == 200 and msg == "success"
    extract:
      token: $.data.token

经过 specification_yaml 处理后:

  • ${get_password()} 会被替换成实际密码。
  • 请求会携带正确的 JSON 数据。
  • 响应中的 token 会被提取保存。
  • 断言会检查是否返回预期结果。

4.断言判断

相等断言模式代码示例
    def equal_assert(self, expected_results, actual_results, status_code=None):
        """
        相等断言模式
        :param expected_results: 预期结果,yaml文件validation值
        :param actual_results: 接口实际响应结果
        :return:
        """
        flag = 0
        if isinstance(actual_results, dict) and isinstance(expected_results, dict):
            # 找出实际结果与预期结果共同的key
            common_keys = list(expected_results.keys() & actual_results.keys())[0]
            # 根据相同的key去实际结果中获取,并重新生成一个实际结果的字典
            new_actual_results = {common_keys: actual_results[common_keys]}
            eq_assert = operator.eq(new_actual_results, expected_results)
            if eq_assert:
                logs.info(f"相等断言成功:接口实际结果:{new_actual_results},等于预期结果:" + str(expected_results))
                allure.attach(f"预期结果:{str(expected_results)}\n实际结果:{new_actual_results}", '相等断言结果:成功',
                              attachment_type=allure.attachment_type.TEXT)
            else:
                flag += 1
                logs.error(f"相等断言失败:接口实际结果{new_actual_results},不等于预期结果:" + str(expected_results))
                allure.attach(f"预期结果:{str(expected_results)}\n实际结果:{new_actual_results}", '相等断言结果:失败',
                              attachment_type=allure.attachment_type.TEXT)
        else:
            raise TypeError('相等断言--类型错误,预期结果和接口实际响应结果必须为字典类型!')
        return flag
方法说明

方法名称

def equal_assert(self, expected_results, actual_results, status_code=None)

功能描述

  • 该方法用于实现 相等断言(Equal Assertion),即验证接口返回的实际结果中某个字段的值是否 完全等于预期值。
  • 通过对比两个字典对象(实际结果与预期结果)中的指定字段内容,判断其是否完全一致。

示例场景
  • 验证接口返回的 code 是否为 200
  • 验证响应中的 user.id 是否等于预期值
  • 验证嵌套结构中的某个字段是否完全匹配预期值

参数说明
参数名类型描述
expected_resultsdict预期结果,通常来自 YAML 文件中的 validation 字段,例如:{“code”: 200}
actual_resultsdict接口实际返回的 JSON 响应体
status_codeintHTTP 状态码(当前未使用,且参数名存在拼写错误)
返回值说明
  • 返回一个整数类型 flag:0表示断言成功,1表示断言失败

执行逻辑详解
1. 初始化标志位
flag = 0

默认表示所有断言通过。


2. 类型校验
if isinstance(actual_results, dict) and isinstance(expected_results, dict):
  • 如果传入的 actual_results 和 expected_results 不是字典类型,则抛出异常:
raise TypeError('相等断言--类型错误,预期结果和接口实际响应结果必须为字典类型!')

3. 获取公共 key(要比较的字段)
common_keys = list(expected_results.keys() & actual_results.keys())[0]
  • 使用集合运算符 & 找出两个字典共有的 key。
  • 取第一个作为比较字段(⚠️ 当前仅支持单字段比较)。

4. 构造新的实际结果字典
new_actual_results = {common_keys: actual_results[common_keys]}
  • 从实际响应中提取对应字段的值,构造一个新的字典。

5. 使用 operator.eq 进行深度比较
eq_assert = operator.eq(new_actual_results, expected_results)
  • operator.eq() 是 Python 的深度比较函数,可以比较复杂数据结构(如嵌套字典、列表等)是否完全相同。
  • 相比 == 更加严格和可靠。

6. 成功/失败处理
# 成功
logs.info("相等断言成功...")
allure.attach(...)

# 失败
flag += 1
logs.error("相等断言失败...")
allure.attach(...)
包含断言模式代码示例
def contains_assert(self, value, response, status_code):
        """
        字符串包含断言模式,断言预期结果的字符串是否包含在接口的响应信息中
        :param value: 预期结果,yaml文件的预期结果值
        :param response: 接口实际响应结果
        :param status_code: 响应状态码
        :return: 返回结果的状态标识
        """
        # 断言状态标识,0成功,其他失败
        flag = 0
        for assert_key, assert_value in value.items():
            if assert_key == "status_code":
                if assert_value != status_code:
                    flag += 1
                    allure.attach(f"预期结果:{assert_value}\n实际结果:{status_code}", '响应代码断言结果:失败',
                                  attachment_type=allure.attachment_type.TEXT)
                    logs.error("contains断言失败:接口返回码【%s】不等于【%s】" % (status_code, assert_value))
            else:
                resp_list = jsonpath.jsonpath(response, "$..%s" % assert_key)
                if isinstance(resp_list[0], str):
                    resp_list = ''.join(resp_list)
                if resp_list:
                    assert_value = None if assert_value.upper() == 'NONE' else assert_value
                    if assert_value in resp_list:
                        logs.info("字符串包含断言成功:预期结果【%s】,实际结果【%s】" % (assert_value, resp_list))
                    else:
                        flag = flag + 1
                        allure.attach(f"预期结果:{assert_value}\n实际结果:{resp_list}", '响应文本断言结果:失败',
                                      attachment_type=allure.attachment_type.TEXT)
                        logs.error("响应文本断言失败:预期结果为【%s】,实际结果为【%s】" % (assert_value, resp_list))
        return flag
方法说明
方法名称
def contains_assert(self, value, response, status_code)

该方法用于实现 字符串包含断言(Contains Assertion),即验证接口响应中是否 包含预期的关键字段和值。支持对状态码和 JSON 响应内容进行断言。


示例场景
  • 验证返回的 msg 是否包含 “登录成功”
  • 验证 HTTP 状态码是否为 200
  • 验证响应中的某个字段是否包含指定值

参数说明
参数名类型描述
valuedict从 YAML 文件中读取的断言规则,例如 { “code”: 200, “msg”: “登录成功” }
responsedict接口实际返回的 JSON 响应体
status_codeint接口返回的 HTTP 状态码
  • 返回一个整数类型 flag:0表示所有断言通过,大于0表示有断言失败,数值代表失败次数

执行逻辑详解
1. 初始化标志位
flag = 0

默认表示所有断言通过。


2. 遍历断言字典
for assert_key, assert_value in value.items():

遍历传入的 value 字典,获取每个断言字段及其预期值。


3. 判断是否为状态码断言
if assert_key == "status_code":
    if assert_value != status_code:
        flag += 1
        allure.attach(...)  # 添加失败信息到 Allure 报告
        logs.error(...)     # 记录错误日志
  • 如果是 “status_code”,则比较预期值与实际状态码。
  • 不相等时标记失败,并记录日志和报告信息。

4. 否则进行响应内容断言(JSONPath 提取 + 包含判断)
resp_list = jsonpath.jsonpath(response, "$..%s" % assert_key)

使用 jsonpath.jsonpath() 从响应数据中提取所有名为 assert_key 的字段值。

response = {"code": 200, "data": {"msg": "登录成功"}}
assert_key = "msg"
resp_list = jsonpath.jsonpath(response, "$..msg")["登录成功"]

对结果做类型处理
if isinstance(resp_list[0], str):
    resp_list = ''.join(resp_list)
  • 如果提取到的是字符串列表,合并成单个字符串方便后续判断。

判断是否为空值(如 NONE)
assert_value = None if assert_value.upper() == 'NONE' else assert_value
  • 支持将字符串 ‘NONE’ 转换为 Python 的 None,便于后续判断空值。

实际断言逻辑
if assert_value in resp_list:
    logs.info("成功")
else:
    flag += 1
    allure.attach(...)
    logs.error("失败")
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐