UI调试平台Web UI自动化项目框架设计

一、项目介绍

该项目是一个UI自动化框架的封装平台,主要基于Python+PlayWright搭建了接口自动化框架,实现注册,登录,新增项目,新增环境等功能用例转自动化的实现,并且合理使用断言,生成可视化的测试报告

技术栈:Python+pytest+playwright+Ajax+allure+Jenkins+Linux

二、项目结构说明

2.1 目录结构

复制代码
web_auto_project
├── cases/                      # 测试用例层:所有测试脚本
│   ├── more_accounts/          # 多账号切换子模块(独立 conftest 管理 admin 上下文)
│   ├── conftest.py             # 用例层 fixture(全局预登录、独立上下文、未登录页)
│   ├── test_login.py           # 登录功能用例
│   ├── test_register.py        # 注册功能用例
│   ├── test_add_project.py     # 新增项目用例(含接口 Mock 场景)
│   ├── test_add_module.py      # 新增模块用例
│   ├── test_project_list.py    # 项目列表用例
│   └── test_lenv_list.py       # 环境列表用例
├── pages/                      # 页面对象层(POM):每个页面一个类,封装元素定位与操作
│   ├── login_page.py
│   ├── register_page.py
│   ├── add_project_page.py
│   ├── add_module_page.py
│   ├── project_list_page.py
│   └── list_env_page.py
├── plugins/                    # 本地 pytest 插件(改造版官方插件)
│   ├── pytest_playwright.py    # Playwright 插件:browser/context/page fixture、录屏、命令行参数
│   └── pytest_base_url_plugin.py  # base-url 插件:统一管理被测环境地址
├── config/                     # 配置层
│   ├── setting.py              # 框架基础参数(路径、日志级别、通知开关),不随环境变化
│   ├── config.yaml             # 环境数据(数据库、Jenkins、钉钉/飞书密钥),随环境变化
│   └── operationConfig.py      # yaml 读写工具类
├── data/                       # 测试数据层
│   └── mock_api.py             # 接口 Mock 数据(URL + 响应处理器)
├── utils/                      # 工具层
│   ├── recordlog.py            # 日志模块(按天输出 + 滚动备份 + 过期清理)
│   ├── dingRobot.py            # 钉钉机器人推送
│   ├── feishuRobot.py          # 飞书机器人推送
│   ├── jenkins_handler.py      # Jenkins 构建信息查询与报告统计
│   └── tools.py                # 通用工具(JSON 数据文件读取)
├── logs/                       # 运行日志输出目录
├── reports/                    # allure 原始结果数据(json)
├── report/                     # JUnit XML 报告目录(results.xml,Jenkins 邮件用)
├── allure_report/              # allure 生成的最终 HTML 报告
├── conftest.py                 # 根 conftest:插件注册、allure 动态标题、失败截图/录屏附加、结束通知
├── pytest.ini                  # pytest 全局配置(命令行参数、base-url)
├── run.py                      # 程序入口:执行用例 + 生成报告
└── requirements.txt            # 依赖清单

2.2 分层设计思想

框架遵循职责分离原则,自上而下分为五层:

  1. 入口层(run.py:统一调度执行流程,屏蔽底层细节
  2. 用例层(cases):只描述"测试什么"------业务步骤编排与断言,不关心元素定位细节
  3. 页面对象层(pages):只描述"页面长什么样"------元素定位器与页面操作方法,被用例层复用
  4. 数据层(data) :存放接口 Mock 数据,用例通过 page.route(**mock_api.xxx) 一行代码注入异常场景
  5. 支撑层(config / plugins / utils):配置管理、浏览器生命周期管理、日志与消息通知,为上层提供基础设施

这种分层使得:页面改版只需改 pages 层;换环境只需改 config.yaml;新增用例只需在 cases 层组合已有页面对象,维护成本大幅降低。

2.3 框架执行流程

复制代码
run.py 启动
   │
   ├─ 1. 重建 report 目录(存放 JUnit XML)
   ├─ 2. pytest.main() 拉起用例执行
   │      ├─ pytest.ini 加载全局参数(--headed、--video、--base-url 等)
   │      ├─ 根 conftest.py 注册 plugins 本地插件
   │      ├─ 插件构建 playwright → browser → context → page fixture 链
   │      ├─ cases 层用例通过 fixture 拿到 page,调用 pages 层页面对象执行操作
   │      ├─ 用例失败 → 钩子自动截图/录屏附加到 Allure 报告
   │      └─ 全部执行完 → pytest_terminal_summary 钩子推送钉钉/飞书通知
   ├─ 3. allure generate 生成 HTML 报告
   └─ 4. allure open 打开报告

三、核心代码介绍

3.1 程序入口 run.py

框架的统一入口,负责整个执行链路的调度:

python 复制代码
import os
import shutil
import pytest
from config import setting
from utils.recordlog import logs

if __name__ == '__main__':

    # JUnit XML 报告路径:Jenkins 邮件模板靠它提取用例统计信息,单独存放在 report 目录,
    result_xml_dir = setting.FILE_PATH['RESULTXML']
    shutil.rmtree(result_xml_dir, ignore_errors=True)
    os.makedirs(result_xml_dir)
    result_xml = os.path.join(result_xml_dir, 'results.xml')

    # 运行测试用例(--clean-alluredir 会在执行前自动清空 reports 目录,无需手动清理)
    logs.info("开始执行测试用例...")
    pytest.main(['--alluredir', './reports', '--clean-alluredir', f'--junitxml={result_xml}'])

    # 生成测试报告
    logs.info("测试用例执行完成,开始生成 Allure 报告...")
    os.system('allure generate ./reports -o ./allure_report --clean')

    # 打开报告
    logs.info("Allure 报告生成完成,正在打开报告...")
    os.system('allure open ./allure_report')

代码说明:

清理并重建 report 目录

python 复制代码
result_xml_dir = setting.FILE_PATH['RESULTXML']
shutil.rmtree(result_xml_dir, ignore_errors=True) # 递归删除旧目录,忽略不存在的情况
os.makedirs(result_xml_dir)
result_xml = os.path.join(result_xml_dir, 'results.xml')

results.xml 是给 Jenkins 邮件模板用的,单独存放在 report/ 目录,与 Allure 的原始数据目录 reports/ 分开,避免混淆。每次运行前清空目录,防止旧文件残留导致统计信息不准

执行测试用例

python 复制代码
pytest.main(['--alluredir', './reports', '--clean-alluredir', f'--junitxml={result_xml}'])
参数 作用
--alluredir ./reports Allure 原始数据写入 ./reports 目录
--clean-alluredir 执 执行前自动清空该目录,无需手动 shutil.rmtree
--junitxml=... 同时输出 JUnit XML 格式报告,供 Jenkins 解析

生成 Allure 报告

python 复制代码
os.system('allure generate ./reports -o ./allure_report --clean')
os.system('allure open ./allure_report') # 启动本地浏览器查看报告

| 参数 | 作用 |
| ./reports | 读取上一步生成的原始数据 |
| -o ./allure_report | 输出静态 HTML 报告到 ./allure_report |

--clean 生成前先清空输出目录,覆盖旧报告

3.2 配置层:setting.py + config.yaml + operationConfig.py

setting.py

python 复制代码
# 基础路径
DIR_BASE = os.path.dirname(os.path.dirname(__file__))
sys.path.append(DIR_BASE)

# log日志输出级别,可选值:DEBUG, INFO, WARNING, ERROR, CRITICAL
LOG_LEVEL = logging.DEBUG  # 文件
STREAM_LOG_LEVEL = logging.DEBUG  # 控制台

# 是否发送钉钉消息
DD_MSG = True

# 是否发送飞书消息
FS_MSG = True

# 是否在钉钉/飞书通知中附加 Jenkins 构建信息(构建编号、构建地址、Allure 报告链接)
JENKINS_ENHANCE = True

# 文件路径
FILE_PATH = {
    'CONFIG': os.path.join(DIR_BASE, 'config/config.yaml'),
    'LOG': os.path.join(DIR_BASE, 'logs'),
    'YAML': os.path.join(DIR_BASE),
    'RESULTXML': os.path.join(DIR_BASE, 'report'),
}

config.yaml

python 复制代码
# 测试环境配置
environment:
  type: 测试环境
  project: UI调试平台WEB自动化项目
  description: 这是一个测试环境配置文件

# MySQL 数据库配置
MYSQL:
  host: http://127.0.0.1
  port: 3306
  username: root
  password: root
  database: test

# 报告类型:默认 allure,若设为 tm 则生成 tmreport 报告
REPORT_TYPE:
  type: allure

JENKINS:
  url: http://127.0.0.1:8080
  username: admin
  password: '123456'  # 必须加引号,否则 YAML 解析为整数,python-jenkins 会报 encode 错误
  job_name: web_auto_project_task  # 测试任务名称
  timeout: 60

# 钉钉通知配置
DING_DING:
  WEBHOOK_URL: xxx
  SECRET: xxx

# 飞书通知配置
FEI_SHU:
  WEBHOOK_URL: xxx
  SECRET: xxx

operationConfig.py

python 复制代码
    def get_item_value(self, section_name):
        """
        根据 yaml 文件的顶级 key 获取该段下所有键值对
        :param section_name: 顶级段名
        :return: 以字典形式返回
        """
        return dict(self.conf.get(section_name, {}))

    def get_section_for_data(self, section, option):
        """
        根据 section 和 option 获取对应的配置值
        :param section: 顶级段名
        :param option: 段下的键名
        :return: 配置值,读取失败时返回空字符串
        """
        try:
            return self.conf[section][option]
        except (KeyError, TypeError) as e:
            logs.error(traceback.format_exc())
            return ''

    def write_config_data(self, section, option_key, option_value):
        """
        向 yaml 配置文件中写入数据(仅当 section 不存在时写入)
        :param section: 顶级段名
        :param option_key: 键名
        :param option_value: 键值
        """
        if section not in self.conf:
            self.conf[section] = {option_key: option_value}
            with open(self.__filepath, 'w', encoding='utf-8') as f:
                yaml.dump(self.conf, f, allow_unicode=True, default_flow_style=False)
        else:
            logs.info(f'"{section}" 值已存在,写入失败')

配置分为两类,边界清晰:

  • setting.py(框架级,不随环境变) :基础路径 DIR_BASE、日志级别、钉钉/飞书通知开关 DD_MSG/FS_MSG、Jenkins 增强开关,以及统一的路径字典 FILE_PATH
  • config.yaml(环境级,随环境变):MySQL 连接、报告类型、Jenkins 地址与账号、钉钉/飞书机器人 Webhook 与密钥。切换测试环境只需替换此文件,代码零改动
  • operationConfig.py :封装 OperationConfig 类,提供按段取值(get_section_for_data)、整段读取、写回配置等方法,读取失败时记录堆栈并安全降级,不让配置问题阻断测试

3.3 本地插件层:pytest_playwright.py

基于官方 pytest-playwright 改造的本地插件,是浏览器生命周期的核心:

  • fixture 链playwright(session 级)→ browser_typebrowsercontextpage,逐层注入
  • 命令行参数 :通过 pytest_addoption 注册 --browser--headed--video--tracing--screenshot--output
  • 录屏管理browser_context_args 中按 --video 配置开启录像;公共函数 save_video_artifacts 在用例结束、页面关闭后保存录屏,并把路径暂存到 request.node._video_artifacts,交由根 conftest 的钩子统一附加到 Allure 用例主体
  • Tracingcontext fixture 中按 --tracing=retain-on-failure 开启,失败时保存 trace.zip

配套的 pytest_base_url_plugin.py 提供 base_url fixture,被测地址在 pytest.ini 中通过 --base-url 统一配置,页面对象里的 page.goto("/login.html") 均为相对路径,换环境只改一处配置

3.4 根 conftest.py:钩子与全局增强

python 复制代码
# 本地插件注册
pytest_plugins = ['plugins.pytest_playwright', 'plugins.pytest_base_url_plugin']

def pytest_runtest_call(item: Item):
    if item.parent._obj.__doc__:
        allure.dynamic.feature(item.parent._obj.__doc__)
    if item.function.__doc__:
        allure.dynamic.title(item.function.__doc__)


@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
    """
    把失败用例的截图/录屏附加到 allure 用例主体。
    fixture teardown 里的 attach 只会挂到 after-fixture(container json),
    报告正文看不到,因此统一在这里附加:
    - call 阶段:用例失败时页面尚未关闭,直接截图并附加到用例主体;
    - teardown 阶段:录屏文件已由 fixture 保存并暂存路径,此处统一附加到用例主体。
    """
    report = (yield).get_result()
    if report.when == "call" and report.failed:
        screenshot_option = item.config.getoption("--screenshot")
        if screenshot_option in ("on", "only-on-failure"):
            page = item.funcargs.get("page") or item.funcargs.get("unlogin_page")
            if page is not None:
                try:
                    screenshot_path = os.path.join(
                        item.config.getoption("--output"),
                        slugify(item.nodeid),
                        "test-failed-1.png",
                    )
                    page.screenshot(timeout=5000, path=screenshot_path)
                    logs.info(f"用例失败,保存截图到用例主体:{screenshot_path}")
                    allure.attach.file(
                        screenshot_path,
                        name=f"{item.name}-failed-1",
                        attachment_type=allure.attachment_type.PNG,
                    )
                except Exception as e:
                    logs.warning(f"失败用例保存截图失败:{e}")
    elif report.when == "teardown":
        for video_path in getattr(item, "_video_artifacts", []):
            try:
                allure.attach.file(
                    video_path,
                    name=os.path.basename(video_path),
                    attachment_type=allure.attachment_type.WEBM,
                )
                logs.info(f"附加用例录屏到报告主体:{video_path}")
            except Exception as e:
                logs.warning(f"附加用例录屏失败:{video_path},原因:{e}")


@pytest.fixture(scope="session")
def browser_type_launch_args(pytestconfig: Any) -> Dict:
    """覆盖官方插件的配置,添加窗口最大化参数"""
    launch_options = {}
    headed_option = pytestconfig.getoption("--headed")
    if headed_option:
        launch_options["headless"] = False
    else:
        launch_options["headless"] = True  # 默认显示浏览器窗口
    # 添加窗口最大化
    launch_options["args"] = ["--start-maximized"]
    return launch_options

# 钩子函数,测试结束后执行
def pytest_terminal_summary(terminalreporter, exitstatus, config):
    """测试结束后收集结果摘要,并按配置推送通知"""
    total = terminalreporter._numcollected
    passed = len(terminalreporter.stats.get('passed', []))
    failed = len(terminalreporter.stats.get('failed', []))
    error = len(terminalreporter.stats.get('error', []))
    skipped = len(terminalreporter.stats.get('skipped', []))
    duration = terminalreporter._session_start.elapsed().seconds

    summary = (
        f"自动化测试结果,通知如下,请着重关注测试失败的接口,具体执行结果如下:\n"
        f"测试用例总数:{total}\n"
        f"测试通过数:{passed}\n"
        f"测试失败数:{failed}\n"
        f"错误数量:{error}\n"
        f"跳过执行数量:{skipped}\n"
        f"执行总时长:{duration}"
    )
    logs.info(f"测试执行完成,结果摘要:\n{summary}")

    if DD_MSG:
        send_dd_msg(summary)
    if FS_MSG:
        send_feishu_msg(summary)

插件注册

python 复制代码
pytest_plugins = ['plugins.pytest_playwright', 'plugins.pytest_base_url_plugin']

配合 pytest.ini-p no:playwright -p no:base_url 禁用官方同名插件,实现本地插件接管

Allure 动态报告结构

python 复制代码
def pytest_runtest_call(item: Item):
    # 动态添加测试类的allure.feature()
    if item.parent._obj.__doc__:
        allure.dynamic.feature(item.parent._obj.__doc__)
    # 动态添加测试用例的title标题allure.title()
    if item.function.__doc__:
        allure.dynamic.title(item.function.__doc__)

pytest_runtest_call 钩子中,把测试类的描述 作为 allure.feature、测试方法的描述作为 allure.title,用例写好文档字符串即自动生成可读的报告层级

失败现场采集

python 复制代码
def pytest_runtest_makereport(item, call)

pytest_runtest_makereport 钩子在 call 阶段失败时截图(此时页面尚未关闭)、在 teardown 阶段把暂存的录屏附加到用例主体,保证附件出现在 Allure 报告正文而非 after-fixture

结果通知

python 复制代码
# 钩子函数,测试结束后执行
def pytest_terminal_summary(terminalreporter, exitstatus, config):

pytest_terminal_summary 钩子统计总数/通过/失败/错误/跳过与耗时,按 setting.py 开关推送钉钉、飞书;预留了 Jenkins 构建编号与报告链接的增强逻辑

3.5 用例级 cases/conftest.py

python 复制代码
@pytest.fixture(scope="session")
def pre_login(context, base_url) -> None:
    """有些网站网页关闭cookie就失效了,全局登录一次"""
    logs.info(f"全局预登录,测试环境 base_url:{base_url}")
    page = context.new_page()
    LoginPage(page).page.goto("/login.html")
    LoginPage(page).login("p", "123456")
    # 等待登录成功页面重定向
    page.wait_for_url(url='**/index.html')

@pytest.fixture(scope="module")
def fresh_context(browser, browser_context_args: Dict):
    """
    登录注册页面(不依赖于先登录)单独创建独立的 context 上下文
    避免全局先登录加载cookie,导致有些打开登录页直接跳到首页去了
    :return:
    """
    context = browser.new_context(**browser_context_args)
    yield context
    context.close()

@pytest.fixture
def unlogin_page(fresh_context: BrowserContext, pytestconfig: Any, request: pytest.FixtureRequest):
    """
    登录注册页面(不依赖于先登录)单独创建独立的 page 对象
    失败截图由 conftest.py 的 pytest_runtest_makereport 钩子在 call 阶段采集并附加到用例主体,
    录屏在页面关闭后保存并暂存路径,由同一钩子在 teardown 阶段统一附加到用例主体
    """
    pages: List[Page] = []

    def _on_page(page: Page) -> None:
        pages.append(page)

    fresh_context.on("page", _on_page)
    page = fresh_context.new_page()
    yield page
    # 收尾时移除监听器,避免向 module 级 context 重复注册导致 pages 列表不断累积
    fresh_context.remove_listener("page", _on_page)
    page.close()
    # 保存录屏并暂存路径(公共逻辑见插件的 save_video_artifacts 函数),
    # 由根 conftest.py 的 makereport 钩子在 teardown 阶段统一附加到用例主体;
    # 必须在 page.close() 之后,录屏文件才会落盘
    save_video_artifacts(pages, pytestconfig, request)

pre_login ------ 全局预登录

  • 作用域 scope="session":整个测试会话只执行一次
  • 依赖插件提供的 context(Playwright 上下文)和 base_url,打开 /login.html 并用 LoginPage 完成登录,然后等待重定向到 **/index.html 确认登录成功
  • 登录产生的 cookie 保留在 context 中,后续所有用例直接以"已登录"状态运行

fresh_context ------ 干净的独立上下文

  • 作用域 scope="module":每个测试模块只执行一次
  • 直接用 browser.new_context() 新建一个没有全局登录 cookie 的上下文
  • 目的:如果登录/注册用例复用全局已登录的 context,打开登录页时网站会自动跳转到首页,导致登录页用例无法测试。这个干净上下文就是为它们隔离环境用的
  • 使用 yield + context.close() 保证模块结束后资源被释放

unlogin_page ------ 未登录状态的页面对象

  • 基于 fresh_context 创建 page,供登录/注册用例使用
  • 监听 page 事件:把 context 中出现的所有页面收集到 pages 列表,确保录屏能覆盖所有页面
  • yield 后清理顺序很讲究:
    1. 先 remove_listener 移除监听器------因为 fresh_context 是 module 级共享的,若不移除,每条用例都会重复注册监听,pages 列表会不断累积
    2. 再 page.close()------Playwright 只有在页面关闭后,录屏文件才会真正落盘
    3. 最后调用 save_video_artifacts 保存录屏并暂存路径
  • 截图和录屏的附加时机:失败截图由根 conftest.py 的 pytest_runtest_makereport 钩子在 call 阶段采集;录屏则在 teardown 阶段由同一钩子统一附加到 Allure 报告,这里只负责保存并暂存路径

3.6 页面对象层(POM):pages/*.py

page 类用来封装具体页面的元素定位和元素操作方法

python 复制代码
class LoginPage:
    def __init__(self, page: Page):
        self.page = page
        # 元素定位
        self.locator_username = page.get_by_label("用 户 名:")
        self.locator_login_btn = page.locator('text=立即登录')
        ...

    # 元素操作
    def navigate(self):
        self.page.goto("/login.html")

    def login(self, username, password) -> None:
        """完整登录操作"""
        self.locator_username.fill(username)
        ...

page 层的编写约定:

  • 构造器 :注入 page 并集中声明元素定位器,优先使用 get_by_label、文本定位等语义化定位方式
  • navigate() 页面跳转入口,路径基于 base_url
  • 业务方法 :封装单元素操作(fill_username)与组合操作(login
  • 定位器以 self.locator_xxx 暴露,用例层可直接对元素做断言

3.7 用例层:cases/*.py

python 复制代码
class TestAddProject:
    """新增项目页面"""            # 类 docstring → allure.feature

    @pytest.fixture(autouse=True)
    def start_for_each(self, pre_login, page: Page):
        self.add_project = AddProjectPage(page)
        self.add_project.navigate()
        yield

    def test_02_add_project_fail_name_exist_400(self, page: Page):
        """新增项目失败-项目名称已存在"""   # 方法 docstring → allure.title
        self.add_project.input_project("t", "test", "test")
        page.route(**mock_api.mock_project_400)   # 一行代码 Mock 接口返回 400
        self.add_project.click_save_button()
        expect(self.add_project.locator_boot_box).to_contain_text('已存在')

用例层的编写约定:

  • 前置统一autouse fixture 完成页面对象初始化与导航,每条用例只写业务步骤
  • 断言 :使用 Playwright 的 expect() 自动重试断言(等待元素可见/文本出现),天然抗页面加载延迟;
  • 接口 Mock :异常场景(名称重复 400、服务端错误 500、无权限 403)全部由 data/mock_api.py 的 Mock 数据驱动,page.route 拦截网络请求并返回预设响应,无需真实后端配合即可覆盖异常分支
  • 参数化 :边界值场景使用 @pytest.mark.parametrize 批量驱动
  • 多账号cases/more_accounts/ 子包通过独立 conftest.py 创建 admin 账号上下文,实现多账号切换测试

3.8 工具层:日志与消息通知

日志记录:recordlog.py

python 复制代码
# 日志目录与文件路径
log_path = setting.FILE_PATH["LOG"]
os.makedirs(log_path, exist_ok=True)
logfile_name = os.path.join(log_path, f"test.{datetime.datetime.now():%Y%m%d}.log")

class RecordLog:
    """日志模块:负责清理过期日志并创建 logger 实例"""

    # 日志保留天数,超过自动清理
    LOG_RETENTION_DAYS = 30
    # 单个日志文件最大字节数(5MB)
    MAX_BYTES = 5 * 1024 * 1024
    # 日志文件滚动备份数量
    BACKUP_COUNT = 7

    def __init__(self):
        self._clean_overdue_logs()
        self.logger = self._create_logger()

    def _clean_overdue_logs(self):
        """清理超过保留天数的日志文件"""
        cutoff = (datetime.datetime.now() - datetime.timedelta(days=self.LOG_RETENTION_DAYS)).timestamp()
        for filename in os.listdir(log_path):
            # 遍历日志目录下的所有文件,拆分文件名和拓展名,过滤掉无扩展名的文件
            if os.path.splitext(filename)[1]:
                filepath = os.path.join(log_path, filename)
                # 获取文件创建时间,若早于截止时间则删除
                if os.path.getctime(filepath) < cutoff:
                    os.remove(filepath)

    def _create_logger(self):
        """创建并配置 logger(文件 + 控制台双输出)"""
        logger = logging.getLogger(__name__)
        # 防止重复打印日志
        if not logger.handlers:
            logger.setLevel(setting.LOG_LEVEL)
            fmt = logging.Formatter(
                '%(levelname)s - %(asctime)s - %(filename)s:%(lineno)d -[%(module)s:%(funcName)s] - %(message)s'
            )

            # 文件输出:按大小滚动备份
            fh = RotatingFileHandler(
                filename=logfile_name, mode='a',
                maxBytes=self.MAX_BYTES, backupCount=self.BACKUP_COUNT, encoding='utf-8'
            )
            fh.setLevel(setting.LOG_LEVEL)
            fh.setFormatter(fmt)
            # 将相应的handler添加在logger对象中
            logger.addHandler(fh)
            # 控制台输出
            sh = logging.StreamHandler()
            sh.setLevel(setting.STREAM_LOG_LEVEL)
            sh.setFormatter(fmt)
            logger.addHandler(sh)
        return logger

logs = RecordLog().logger

RecordLog 类按天生成日志文件(logs/test.YYYYMMDD.log),双通道输出(文件 + 控制台),文件日志按 5MB 滚动备份、保留 30 天自动清理

机器人通知: dingRobot.py / feishuRobot.py

python 复制代码
# 读取钉钉机器人配置
_config = OperationConfig()
WEBHOOK_URL = _config.get_section_for_data('DING_DING', 'WEBHOOK_URL')
SECRET = _config.get_section_for_data('DING_DING', 'SECRET')

def generate_sign():
    """
    计算钉钉机器人加签签名
    签名字符串: timestamp + "\n" + 密钥,使用HmacSHA256算法计算签名,再Base64编码后urlEncode
    :return: (当前时间戳, 签名)
    """
    timestamp = str(round(time.time() * 1000))
    # 拼接签名字符串并计算HmacSHA256签名
    str_to_sign = f'{timestamp}\n{SECRET}'
    hmac_code = hmac.new(
        SECRET.encode('utf-8'),
        str_to_sign.encode('utf-8'),
        digestmod=hashlib.sha256
    ).digest()
    # Base64编码后URL编码
    sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
    return timestamp, sign


def send_dd_msg(content_str, at_all=True):
    """
    向钉钉机器人推送消息
    :param content_str: 发送的内容
    :param at_all: 是否@全员,默认为True
    :return: 接口响应文本
    """
    timestamp, sign = generate_sign()
    # url(钉钉机器人Webhook地址) + timestamp + sign
    url = f'{WEBHOOK_URL}&timestamp={timestamp}&sign={sign}'
    headers = {'Content-Type': 'application/json;charset=utf-8'}
    data = {
        "msgtype": "text",
        "text": {"content": content_str},
        "at": {"isAtAll": at_all}
    }
    try:
        res = requests.post(url, json=data, headers=headers)
        # 钉钉接口返回 errcode 为 0 时才表示推送成功(HTTP 200 不代表推送成功)
        if res.json().get('errcode') == 0:
            logs.info(f"钉钉通知推送成功,响应:{res.text}")
        else:
            logs.error(f"钉钉通知推送失败,响应:{res.text}")
        return res.text
    except Exception as e:
        logs.error(f"钉钉通知推送异常:{e}")
        return ''

实现机器人加签(HmacSHA256 + Base64 + URL 编码)与消息推送,以接口返回的 errcode 判定推送成败

Jenkins 操作:jenkins_handler.py

python 复制代码
class JenkinsHandler:
    """Jenkins 操作类,封装构建查询、状态获取、测试报告统计等功能"""

    def __init__(self):
        conf = OperationConfig()
        # 强制转 str:YAML 会把纯数字密码/用户名解析为整数,而 python-jenkins 要求字符串(内部调 encode)
        self.__server = jenkins.Jenkins(
            url=str(conf.get_section_jenkins('url')),
            username=str(conf.get_section_jenkins('username')),
            password=str(conf.get_section_jenkins('password')),
            timeout=int(conf.get_section_jenkins('timeout'))
        )
        self.job_name = conf.get_section_jenkins('job_name')

    def get_job_number(self):
        """读取当前 job 的最新构建号"""
        return self.__server.get_job_info(self.job_name)['lastBuild']['number']

    def get_build_job_status(self):
        """读取最新构建的状态"""
        build_num = self.get_job_number()
        return self.__server.get_build_info(self.job_name, build_num)['result']

    def get_console_log(self):
        """获取最新构建的控制台日志"""
        return self.__server.get_build_console_output(self.job_name, self.get_job_number())

    def get_job_description(self):
        """返回 job 描述信息和 URL(单次请求获取)"""
        job_info = self.__server.get_job_info(self.job_name)
        return job_info['description'], job_info['url']

    def get_build_report(self):
        """获取最新构建的测试报告"""
        return self.__server.get_build_test_report(self.job_name, self.get_job_number())

    def get_build_enhance_info(self):
        """
        获取构建编号、构建地址与 Allure 报告链接等链接类信息
        构建进行中即可查询,用于增强钉钉/飞书通知内容;
        最终构建结果在构建结束前未知,故此处仅提供链接
        :return: 包含 build_number、build_url、allure_url 的字典
        """
        build_num = self.get_job_number()
        job_url = self.__server.get_job_info(self.job_name)['url']
        build_url = f'{job_url}{build_num}/'
        logs.info(f"查询 Jenkins 构建信息成功:第{build_num}次,构建地址:{build_url}")
        return {
            'build_number': build_num,
            'build_url': build_url,
            # Allure Jenkins 插件生成报告页的固定相对路径
            'allure_url': f'{build_url}allure/'
        }

基于 python-jenkins 封装 Jenkins 操作,可查询最新构建号、构建状态、控制台日志、测试报告统计(通过/失败/跳过/耗时),并从日志中正则提取 Allure 报告链接,用于增强通知内容

四、测试结果

4.1. 执行结果

用例数量

  • 源码里的 test_ 方法数:57 条

  • 最近一次 Allure 实际执行数:66 条(因为参数化展开)

分模块执行统计(Allure):

  • 项目列表:12

  • 环境列表:13

  • 登录功能:12

  • 注册功能:12

  • 添加项目:9

  • 添加模块:7

  • 多账号:1

最近一次执行结果

  • 总计 66

  • 通过 64

  • 失败 2

4.2. Allure报告

4.3. 邮箱通知

4.4.飞书/钉钉通知

五、项目结语

本项目基于 Playwright + pytest + Allure 构建了一套完整、稳定、可扩展的 Web UI 自动化测试框架,适用于调试平台(Debug 平台)的业务流程测试。框架采用「配置层 --- 页面对象层 --- 用例层 --- 数据层 --- 工具层」五层分层架构,通过页面对象模式(Page Object Model)将元素定位与业务操作解耦;借助数据驱动、参数化、Mock 接口拦截、登录态隔离(全局预登录与干净上下文分离)等机制保证用例的独立性与可复用性;同时集成失败自动截图、用例录屏、Allure 报告可视化以及钉钉/飞书机器人通知、Jenkins 持续集成,实现了从用例执行到结果推送的全流程自动化,有效提高了测试效率与质量

相关推荐
天远数科37 分钟前
零信任架构实战:基于天远风控经营异常预警构建自动化企业准入网关
运维·人工智能·架构·自动化
今天AI了吗1 小时前
AI工作流的自动化趋势:从手动实验到自主Agent的研究范式转变
运维·数据库·人工智能·sql·机器学习·自动化·github
测试秃头怪1 小时前
Postman中变量的使用
自动化测试·软件测试·python·测试工具·测试用例·接口测试·postman
青山科技分享1 小时前
跨境电商AI Agent哪个比较好用?剖析自动化运营工具的落地价值
运维·人工智能·自动化·ai智能体
迁移科技3 小时前
3D视觉引导销轴上下料:单相机双工位高效方案
人工智能·自动化·视觉检测
AI行业说3 小时前
2026年家纺智能模板机品牌排名:六大品牌实测对比,枕套窗帘沙发垫工厂自动化选型指南
自动化·智能模板机·上橡筋机
戴西软件3 小时前
远程协同仿真是什么体验?
运维·jvm·人工智能·自动化·rpa
adinnet202613 小时前
财务报销自动化:用智能体辅助处理繁琐报销任务
运维·自动化
破土士V14 小时前
【软件测试】自动化测试
自动化测试·selenium·web自动化测试·ui自动化