一.项目介绍

这是一个经典的电商业务场景,基于Python的接口自动化测试框架,该项目技术栈包括:Python+pytest+sqlalchemy+requests+allure+jsonpath+yaml+Jenkins+Linux

该项目是一个在线购物的商城网站,包括用户注册,登录,下单,上架/下架商品,下单支付等相关功能。

二.项目结构说明

1、项目整体框架

本项目采用清晰的分层架构,将核心操作、工具方法、测试用例和数据彻底分离。这种“高内聚、低耦合”的设计,极大地提升了框架的可维护性和可扩展性。核心目录结构如下:

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                              # 主程序入口(执行测试和生成报告)

2、驱动层 +用例层+数据层

本自动化测试项目框架拆分成三层:驱动层 +用例层+数据层。

驱动层:通俗来讲(怎么做)。提供技术实现和业务操作封装。主要体现在 base/ 目录下。

用例层 : (做什么)。组织测试场景,描述业务流程。testcases/ 目录下。

数据层 :(用什么数据做)。存储和管理测试数据。data/ 目录下的YAML/JSON/Excel文件。

三.核心代码

1. 程序入口 run.py

项目通过 run.py 作为统一入口执行测试,支持生成Allure报告并自动打开浏览器查看。核心代码如下:

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')
代码说明:
主程序入口
# run.py - 测试框架主入口
if __name__ == '__main__':
    # 当脚本作为主程序运行时,执行测试流程
    # 此条件判断防止模块被导入时意外执行测试逻辑
   

完整的入口程序不仅包含执行逻辑,还需要处理不同类型的报告生成。下面是针对Allure报告的详细配置:

Allure 报告处理逻辑
if REPORT_TYPE == 'allure':
    pytest.main([
        '-s', '-v',                      # 输出详细日志
        '--alluredir=./report/temp',     # 指定Allure原始数据目录
        './testcase',                    # 指定测试用例目录
        '--clean-alluredir',             # 清理历史数据
        '--junitxml=./report/results.xml' # 同时生成JUnit格式报告
    ])

为增强测试报告的可追溯性,框架会将测试环境信息注入Allure报告。这在多环境测试中尤为重要,可以快速确认测试执行的具体环境。下面是具体说明:

# 复制环境信息文件至Allure原始数据目录
shutil.copy('./config/environment.xml', './report/temp/')

# 作用:Allure报告会读取此文件,在[Environment]板块展示测试环境信息
# 内容示例:<environment><parameter><key>Python</key><value>3.9</value></parameter></environment>
# 生成并展示Allure报告
return_code = os.system('allure serve ./report/temp')

# 命令解析:
# 1. `allure serve`:启动本地Web服务并渲染Allure报告
# 2. `./report/temp`:指定包含Allure原始数据的目录
# 3. 操作系统会自动调用默认浏览器打开报告页面

在持续集成流水线中,通常使用 allure generate 命令生成静态HTML报告并归档,而非使用 allure serve。本项目的 serve 命令主要用于测试调试阶段的即时查看。

2.测试用例 testcase

商务管理代码示例:

本模块位于 testcases/ 层,是POM模式中的“用例层”,职责是描述业务场景,而非实现具体操作。通过调用驱动层(base/)封装,将YAML中描述的测试数据转化为可执行的业务流程验证。

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 参数化装饰器
@pytest.mark.parametrize('case_info', get_testcase_yaml('./testcase/Business interface/BusinessScenario.yml'))

作用:

实现参数化测试,即一个测试方法可以运行多组不同的输入数据,数据储存在YAML文件中。@pytest.mark.parametrize 是项目框架实现 “数据驱动” 的核心技术。它把测试逻辑(怎么写)和测试数据(测什么)分开。pytest运行时会将这一个方法定义,基于YAML中的数据量自动展开为多个独立的测试用例执行,每个都有完整Allure报告。

每一组 case_info 数据都会触发一次完整的测试执行。

示例(YAML 文件内容):

- baseInfo:
    api_name: 获取商品列表
    method: GET
    url: /product/list
- baseInfo:
    api_name: 提交订单
    method: POST
    url: /order/create

测试方法定义
### 测试方法定义

```python
def test_business_scenario(self, case_info):

这是一个 pytest 测试方法,每个参数化的 case_info 都会触发一次该方法的执行。

self 表示这是类中的一个实例方法(属于 TestEBusinessScenario 类)。

case_info 是从 YAML 文件中加载的一条测试用例的数据字典。


动态设置 Allure 报告标题
allure.dynamic.title(case_info['baseInfo']['api_name'])

标题内容来自于 YAML 文件中 baseInfo.api_name 字段

如果 api_name 是 "用户下单成功",那么在Allure报告中显示的用例标题就是"用户下单成功"。


执行接口请求和校验
RequestBase().specification_yaml(case_info)

这个代码是POM模式中的核心调用,体现了框架的重点——封装、可维护性

用例层(这里)只负责下达指令:命令RequestBase()按照提供的case_info(从YAML中读取的测试场景数据),完成从构造请求到验证结果的全套工序。

驱动层(RequestBase)负责具体操作:构造请求、发送请求、处理Cookie、记录日志、解析响应、执行断言。(注:读取case_info,并根据其中的配置(如 URL、方法、请求头、预期结果等)发送 HTTP 请求。)

优势:将接口测试技术细节封装在驱动层。当技术栈或协议细节变更时,仅需修改底层驱动,所有上层用例无需任何改动,极大提升了框架的可维护性。(如:当HTTP客户端库从 requests 更换为 httpx,只需修改 RequestBase类,测试用例完全不需要改动。)

3.接口测试    specification_yaml() 方法

创建 RequestBase 实例后,就可以调用其 specification_yaml() 方法处理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

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


方法定义

即通知系统,有一个叫做 specification_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:定义请求参数类型白名单,用于分类YAML中不同用途的参数。

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)

从 test_case 中提取用例名称并删除原始字段。

添加到 Allure 报告中。

下一次运行操作时,会重新从YAML文件中读取完整全新的数据。


5. 处理断言逻辑
val = self.replace_load(test_case.get('validation'))
test_case['validation'] = val
validation = eval(test_case.pop('validation'))

替换断言表达式中的变量。

用 eval 执行断言表达式,生成实际断言规则。(YAML文件是字符串文本,但测试框架需要python数据结构来执行断言。eval() 的作用就是把字符串文本->python数据结构)


6. 提取数据字段(extract / extract_list)
extract = test_case.pop('extract', None)
extract_list = test_case.pop('extract_list', None)

从测试数据中取出并移除数据提取配置,为后续从接口响应中提取值做准备。

extract:取出单值提取配置。字符串、数字等基本类型。token、ID、状态码等。

extract_list:取出列表值提取配置。列表。商品列表、用户列表等。

取出的数据会送到数据提取执行器,最终存入全局变量池。存入后,其他接口可通过 ${变量名} 直接引用,实现数据跨接口流动。


7. 处理请求参数(data/json/params)
for key, value in test_case.items():
    if key in params_type:
        test_case[key] = self.replace_load(value)

将测试用例中的变量占位符(如 ${username})替换为变量池里预先存储的实际值,使测试数据可动态配置。


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')}

如果存在 files 字段,表示是文件上传接口。

使用 open(..., mode='rb') 读取文件内容,并附加到 Allure 报告中。

    比如用户上传的头像,产品图片等等,然后将文件上传这种特殊类型的请求数据与普通请求参数区别开,以二进制模式打开文件,准备HTTP请求。并且在Allure测试报告中附加文件信息,确保文件上传功能的测试可追溯和可验证。


    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

    捕获所有异常,然后重新抛出相同的异常,不做任何处理。


    示例说明

    假设 YAML 文件如下:

    这是登录接口的自动化测试配置,定义了请求参数、响应验证规则和数据提取规则,用于自动化测试登录功能并获取认证token

    baseInfo:
      api_name: 登录接口     # 接口名称
      url: /login           # 接口路径
      method: POST          # 请求方法
      header:
        Content-Type: application/json  # 请求头:JSON格式
      cookies: null         # Cookie(无)
    
    testCase:
      - case_name: 正常登录    # 测试用例名称
        json:                 # 请求体(JSON格式)
          username: admin     # 用户名
          password: ${get_password()}  # 密码(动态获取)
        validation:           # 响应验证规则
          code == 200 and msg == "success"  # 验证状态码和消息
        extract:              # 响应数据提取
          token: $.data.token  # 从响应中提取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

    验证 HTTP 状态码是否符合预期

    验证响应中的 user.id 是否等于预期值

    验证用户名、邮箱等关键信息是否准确

    验证对象中的特定属性是否符合要求


    返回值说明

    返回一个整数类型 flag:
    0 表示断言成功          1 表示断言失败


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

    创建断言结果标志位,默认值 0 表示断言通过状态

    用数字标志位便于后续统计失败数量

    2. 类型校验
    if isinstance(actual_results, dict) and isinstance(expected_results, dict):

      检查 actual_results 和 expected_results 是否均为字典类型

      是方法执行的前提条件,确保数据格式正确

      3. 查找共同字段
      common_keys = list(expected_results.keys() & actual_results.keys())[0]

      使用集合交集操作找出两个字典中共同的键

      通过 & 运算符获取共同键的集合

      转换为列表后取第一个元素作为比较目标

      4. 构造比较字典
      new_actual_results = {common_keys: actual_results[common_keys]}

      基于找到的共同字段,从实际结果中提取对应值

      创建新的字典,只包含要比较的字段键值对

      目的是聚焦比较目标,忽略其他不相关字段

      5. 执行相等比较
      eq_assert = operator.eq(new_actual_results, expected_results)

      用 operator.eq() 函数进行严格的字典相等性比较

      比较构建的实际结果字典与预期结果字典是否完全一致

      6. 结果处理和记录
      if eq_assert:
          # 成功分支:记录成功日志和测试报告
          logs.info(f"相等断言成功...")
          allure.attach(..., '相等断言结果:成功', ...)
      else:
          # 失败分支:更新标志位并记录错误信息
          flag += 1
          logs.error(f"相等断言失败...")
          allure.attach(..., '相等断言结果:失败', ...)

      根据比较结果分别处理成功和失败情况

      成功时记录信息级别日志,失败时记录错误级别日志

      将详细结果附加到 Allure 测试报告中

      7. 异常处理
      else:
          raise TypeError('相等断言--类型错误...')

      当输入参数不是字典类型时抛出类型错误异常

      8. 返回结果
      return flag

      整型标志位:0 表示断言成功,>0 表示断言失败

      四.项目结语

      本自动化接口测试项目基于Python+Pytest构建了一套完整、高效、可维护的测试解决方案,通过模块化设计和智能化处理,显著提升了接口测试的效率与可靠性。

                Logo

                Agent 垂直技术社区,欢迎活跃、内容共建。

                更多推荐