自动化接口框架搭建分享-pytest第二部分

在我们设计出前面的基础底层框架后,我们就其实已经能够写下第一个简单的基本的测试用例

测试用例设计(testset、data)

因此,我们就创建了两个包

  • testset 用于存储py文件,写下接口API测试用例

  • data 用于存储yaml文件,导入对应参数,给接口提供对应的入参

testset 下python文件的写法据我所知给几点参考:

  • 命名规范:- 所有的测试文件都应该以 test_ 开头
  • 一个对应API测试用例要包括:封装好的http请求工具,数据加载工具,断言工具(根据业务需求设计),json(一些数据处理库)
  • 测试数据:从yaml文件中加载

数据加载工具(data_loader)

复制代码
import yaml
import os
from typing import List, Dict

def load_yaml_testdata(filename: str) -> List[Dict]:
    """加载YAML格式的测试数据"""
    data_dir = os.path.join(os.path.dirname(__file__), '../data')
    filepath = os.path.join(data_dir, filename)
    print(data_dir,filepath)
    with open(filepath, 'r', encoding='utf-8') as f:
        return yaml.safe_load(f)

不过总的来说

你可以把 `data_loader.py` 想象成一个 "数据搬运工" 。

它的主要工作就是:

去 data 文件夹里找数据 : 你的测试用例需要各种数据才能运行,比如登录的用户名密码、商品的价格、订单的详情等等。这些数据都放在了 data 文件夹里,而且是用一种叫做 YAML 的文件格式写的(就像一个清单)。

把数据读出来: 这个"数据搬运工"会打开你指定的 YAML 文件,把里面的数据一行一行地读出来。

把数据整理好,交给测试用例 : 读出来的数据它会整理成 Python 能理解的格式(通常是字典和列表),然后交给你的测试用例去使用。

为什么需要这个"数据搬运工"呢?

让测试代码更干净 : 如果把所有测试数据都写在测试代码里,代码会变得又长又乱。有了"数据搬运工",测试代码就只管测试逻辑,数据的事情交给它。

一个测试跑多组数据 : 比如你想测试登录功能,用不同的用户名密码都试一遍。你可以在 YAML 文件里写好几组用户名密码,然后"数据搬运工"会把它们一组一组地拿给测试用例去跑,非常高效。

`data_loader.py` 就是帮你把测试需要的数据,从专门存放数据的文件里,方便、安全地取出来。


测试框架基础(confest.py、pytest.ini)

复制代码
import pytest
from utils.requests_helper import RequestUtil
from utils.mysql_helper import MySQLPlugin
from config.setting import *
from utils.logger_helpper import logger


@pytest.fixture(scope="session", autouse=True)
def auth_token():
    """获取认证token"""
    userId = User_Id

    # 发送获取token的请求
    reqObj = RequestUtil(
        base_url=Test_URL
    )
    method = 'get'
    params = {'userId': userId}

    try:
        response = reqObj.send_request(method, endpoint=Auth_URL, params=params)
        if response.status_code == 200:
            token = response.json().get('data', {})
            # logger.debug(f"Token Info: {token}")
        return token
    except Exception as e:
        # logger.error(f"获取token失败: {str(e)}")
        pytest.fail(f"获取token失败: {str(e)}")


@pytest.fixture(scope="session", autouse=True)
def api_client(auth_token: str):
    """创建带有认证token的请求客户端"""

    # 完成请求头的设置
    headers = {
        'Content-Type': 'application/json',
        'Authorization': f"Bearer {auth_token}"
    }

    # 创建客户端实例
    client = RequestUtil(
        base_url=Test_URL,
        default_headers=headers,
        timeout=30
    )

    yield client

    # 测试结束后可以在这里添加清理逻辑
    logger.debug("测试结束,清理请求客户端")


def pytest_sessionfinish(session):
    """修正后的测试结果统计"""
    # 获取终端报告器
    reporter = session.config.pluginmanager.get_plugin('terminalreporter')

    # 统计实际失败数(排除标记允许失败的用例)
    actual_failures = 0
    if reporter:
        actual_failures = len(reporter.stats.get('failed', []))

    logger.debug(f"实际失败用例数: {actual_failures}")
    # 设置退出码(保留原始退出码逻辑)
    session.exitstatus = 1 if actual_failures == 0 else 1


@pytest.fixture(scope="session", autouse=True)
def mysql_client():
    config = {
        "host": DB_Host,
        "user": DB_User,
        "password": DB_Pwd,
        "port": DB_Port,
        "database": DB_Schema,
    }
    client = MySQLPlugin(config=config)
    yield client
    # 测试结束后可以在这里添加清理逻辑
    client.close()
    logger.debug("测试结束,关闭所有数据库连接池")

confest.py 中所定义的 fixture 和 hook,它们提供了测试前置条件和后置清理的能力。

具体来说,fixture是提供共享的测试资源和功能,他要做的有很多,比如登录凭证、数据库连接、HTTP 客户端等,这些是很多测试用例都需要用到的东西。

hook则是定义测试生命周期中的钩子函数(Hooks): 在测试开始前、结束后,或者在收集测试用例时执行一些特定的操作。

同时,

我们同样在confest.py 中看到导入了对应的mysql_helpper.py,

复制代码
import pymysql
from pymysql import cursors
from typing import Union, List, Dict, Any


class MySQLPlugin:
    """MySQL数据库操作插件

    功能特性:
    - 支持连接池管理
    - 自动提交事务机制
    - 类型提示和参数化查询
    - 灵活的查询结果返回格式
    - 上下文管理器支持

    示例配置:
    config = {
        'host': 'localhost',
        'port': 3306,
        'user': 'root',
        'password': 'secret',
        'database': 'my_db',
        'pool_size': 5
    }
    """

    def __init__(self, config: dict):
        self.config = config
        self.pool = []
        self._create_pool()

    def _create_pool(self):
        """创建连接池"""
        for _ in range(self.config.get('pool_size', 3)):
            conn = pymysql.connect(
                host=self.config['host'],
                port=self.config['port'],
                user=self.config['user'],
                password=self.config['password'],
                database=self.config['database'],
                cursorclass=cursors.DictCursor,
                autocommit=True
            )
            self.pool.append(conn)

    def get_conn(self):
        """从连接池获取连接"""
        if not self.pool:
            self._create_pool()
        return self.pool.pop()

    def release_conn(self, conn):
        """归还连接到连接池"""
        self.pool.append(conn)

    def close(self):
        """关闭连接池"""
        for conn in self.pool:
            conn.close()
        self.pool.clear()

    def execute(self, sql: str, params: Union[tuple, List[tuple]] = None) -> int:
        """执行写操作(INSERT/UPDATE/DELETE)

        Args:
            sql: SQL语句,使用%s作为占位符
            params: 单个元组(单条操作)或元组列表(批量操作)

        Returns:
            受影响的行数
        """
        conn = self.get_conn()
        try:
            with conn.cursor() as cursor:
                rowcount = cursor.execute(sql, params) if isinstance(params, tuple) \
                    else cursor.executemany(sql, params)
                return rowcount
        finally:
            self.release_conn(conn)

    def query(self, sql: str, params: tuple = None, fetch_one: bool = False) -> Union[List[dict], dict]:
        """执行查询操作

        Args:
            fetch_one: 是否只获取第一条结果
        """
        conn = self.get_conn()
        try:
            with conn.cursor() as cursor:
                cursor.execute(sql, params)
                return cursor.fetchone() if fetch_one else cursor.fetchall()
        finally:
            self.release_conn(conn)

    # 以下是快捷方法
    def insert(self, table: str, data: dict) -> int:
        """插入单条数据"""
        columns = ', '.join(data.keys())
        placeholders = ', '.join(['%s'] * len(data))
        sql = f"INSERT INTO {table} ({columns}) VALUES ({placeholders})"
        return self.execute(sql, tuple(data.values()))

    def update(self, table: str, updates: dict, where: str, where_params: tuple) -> int:
        """更新数据"""
        set_clause = ', '.join([f"{k} = %s" for k in updates])
        sql = f"UPDATE {table} SET {set_clause} WHERE {where}"
        return self.execute(sql, tuple(updates.values()) + where_params)

    def delete(self, table: str, where: str, where_params: tuple) -> int:
        """删除数据"""
        sql = f"DELETE FROM {table} WHERE {where}"
        return self.execute(sql, where_params)

    # 事务支持
    def begin_transaction(self):
        """开启事务"""
        conn = self.get_conn()
        conn.autocommit(False)
        return conn

    def commit(self, conn):
        """提交事务"""
        conn.commit()
        self.release_conn(conn)

    def rollback(self, conn):
        """回滚事务"""
        conn.rollback()
        self.release_conn(conn)

    # 上下文管理器支持
    def __enter__(self):
        self.conn = self.begin_transaction()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type:
            self.rollback(self.conn)
        else:
            self.commit(self.conn)


# 数据库相关内容填写
if __name__ == "__main__":
    DB_Host = ""
    DB_User = ""
    DB_Pwd = ""
    DB_Port = 3
    DB_Schema = ""
    config = {
        "host": DB_Host,
        "port": 3,
        "user": DB_User,
        "password": DB_Pwd,
        "database": DB_Schema,
        "pool_size": 3
    }

    db = MySQLPlugin(config)

在攥写这个页面的代码时,相当于你是一个DBA ,你要做到包括能够连接数据库、连接池管理、执行SQL语句、事务管理、结果处理(字典格式)、错误处理、易用性 。其中连接池管理就是意味着每次应该不是新建连接,而是维护一个连接池,重复利用连接;

这个功能有点类似于java的JDBC。

pytest.ini 是 Pytest 测试框架的配置文件。你可以把它想象成 Pytest 的 "使用说明书" 或者 "设置面板" ,比如:

  • 去哪里通过修改这个文件,你可以告诉 Pytest 怎么运行你的测试找测试文件?

  • 测试报告怎么生成?

  • 哪些测试用例需要特殊标记?

    [pytest]
    addopts = -vs --alluredir=./allure-results --clean-alluredir

    addopts = -vs

    testpaths = ./testset/
    ;python_files = test_xxx.py
    python_classes = Test*
    python_functions = test_*

    markers =
    app: 小程序模块测试
    business: 业务模块测试
    common: 公共模块测试
    mall: 商城模块测试
    manager: 管理模块测试
    product: 商品模块测试
    task: 任务模块测试
    smoke: 冒烟测试
    performance: 性能测试
    not_ready: 未完成的接口测试
    sqlcheck: 数据库校验

代码当中:

复制代码
addopts = -vs --alluredir=./allure-results --clean-alluredir与生成Allure报告相关
testpaths、files这些告诉了我们测试用例存放的位置
markers 就是自定义的测试注解,给相关的测试函数打上标签
相关推荐
一个天蝎座 白勺 程序猿6 小时前
KingbaseES数据库归档重做日志文件管理:从配置到运维的实战指南
运维·数据库·oracle·kingbasees·金仓数据库
分布式存储与RustFS10 小时前
镜像仓库的层别堆本地盘:用 RustFS 给 Docker Registry 当 S3 后端
运维·云原生·开源·对象存储·分布式存储·s3·性能基准
陈希12310 小时前
Linux 防火墙配置实战:firewalld、iptables 与云安全组
linux·运维·服务器
@兽血沸腾11 小时前
Windows系统安装Jenkins部署应用至远程Windows服务器操作指南
运维·服务器·jenkins
讯捷蓝达17 小时前
戴尔R720xd服务器卡死在 “Configuring Memory...Done“,换了内存还是卡?问题根本不在内存
运维·服务器
爱和冰阔落19 小时前
【Linux网络】TCP 服务器如何并发处理多个客户端?从 fork、僵尸进程到双重 fork
linux·运维·服务器·网络
lisw0520 小时前
基于神经网络的机器人导航系统的设计与执行!
神经网络·机器人·自动化
lisanmengmeng20 小时前
alertmanger 标签 rules 打上分组标签
linux·运维·服务器
严肃小白兔20 小时前
Cloudflare 更新 API Token 管理:从最小权限到自动化凭据安全
网络·安全·自动化
Android系统攻城狮20 小时前
Linux FFmpeg深度解析之av_write_frame调用流程与实战(三十三)
linux·运维·服务器·ffmpeg·音视频进阶·ffmpeg音视频