一、项目介绍

该项目是面向AI智能问答平台的UI自动化测试框架,主要基于Python、Pytest和Playwright实现核心业务流程的自动化验证,覆盖用户登录、AI对话、文件上传、会话管理、消息操作和后台用户管理等模块;同时结合日志、截图和Allure测试报告,提升测试结果的可追踪性与问题定位效率,并基于Jenkins搭建自动化测试持续集成任务。

技术栈:Python + pytest + playwright + allure + json + Jenkins

二、架构说明

ChatAI-UI-Automation-Testing-Framework/              # 项目根目录
├── auth/                                            # 登录态缓存目录
│   └── auth_state.json                              # 保存登录态文件
│
├── common/                                          # 公共工具模块
│   ├── get_json_data.py                             # JSON 测试数据读取工具
│   ├── log_utils.py                                 # 日志封装与日志滚动处理
│   ├── settings.py                                  # 全局配置项管理
│   └── userchat_info_write.py                       # AI 问答结果写入本地文件
│
├── data/                                            # 测试数据目录
│   ├── __init__.py                                  # 包初始化文件
│   ├── ai_dialogue_data.json                        # AI 对话测试数据
│   ├── login_fail_data.json                         # 登录失败测试数据
│   ├── login_success_data.json                      # 登录成功测试数据
│   └── chatfile/                                    # 文件上传测试资源
│       ├── 你好.jpg                                 # JPG 测试文件
│       ├── 狐狸.jpg                                 # JPG 测试文件
│       ├── 狐狸.png                                 # PNG 测试文件
│       └── re_question                              # 追问/补充测试内容
│
├── logs/                                            # 日志目录
│   └── ui_test.xxxxxxxx.log                         # 自动化执行日志
│
├── pages/                                           # POM 页面对象层
│   ├── add_users_page.py                            # 后台新增用户页面对象
│   ├── ai_dialogue_page.py                          # AI 对话页面对象
│   ├── ai_question_answering_process_page.py        # AI 问答流程页面对象
│   ├── exit_login.py                                # 退出登录页面对象
│   ├── login_page.py                                # 登录页面对象
│   ├── session_manage_page.py                       # 会话管理页面对象
│   └── upload_file_operation_page.py                # 文件上传页面对象
│
├── testcases/                                       # 测试用例层
│   ├── addusers/                                    # 后台用户管理测试用例
│   │   ├── __init__.py                              # 包初始化文件
│   │   └── test_add_users.py                        # 新增用户测试
│   │
│   ├── aichat/                                      # AI 对话相关测试用例
│   │   ├── test_ai_chat_process.py                  # AI 问答主流程测试
│   │   ├── test_ai_dialogue.py                      # AI 对话功能测试
│   │   ├── test_ai_message_copy.py                  # 消息复制功能测试
│   │   ├── test_ai_message_regenerate.py            # 消息重新生成功能测试
│   │   └── test_ai_message_stop.py                  # 停止回复功能测试
│   │
│   ├── exit_login/                                  # 退出登录测试用例
│   │   └── test_exit_login.py                       # 退出登录测试
│   │
│   ├── login/                                       # 登录模块测试用例
│   │   ├── test_login.py                            # 登录基础功能测试
│   │   ├── test_login_ajax.py                       # Ajax 请求与响应断言测试
│   │   ├── test_login_params.py                     # 参数化登录测试
│   │   └── test_mock_login.py                       # Mock 异常登录测试
│   │
│   ├── session_manage/                              # 会话管理测试用例
│   │   └── test_session_manage.py                   # 会话重命名/删除测试
│   │
│   └── upload_file_operation/                       # 文件上传测试用例
│       └── test_upload_file_operation.py            # 文件上传功能测试
│
├── test_results/                                    # 测试结果输出目录
│   ├── add_users/                                   # 新增用户测试结果
│   ├── aichat_process_record/                       # AI 问答流程执行结果
│   ├── ai_dialogue_record/                          # AI 对话/消息操作结果
│   ├── exit_login/                                  # 退出登录截图结果
│   ├── fail_screenshots/                            # 用例失败截图
│   ├── login/                                       # 登录测试截图结果
│   ├── session_manage/                              # 会话管理截图结果
│   └── upload_file/                                 # 文件上传截图结果
│
├── allure-results/                                  # Allure 原始测试结果目录
├── conftest.py                                      # 全局 fixture 配置(浏览器、登录态、页面上下文)
├── pytest.ini                                       # Pytest 配置文件
├── requirements.txt                                 # 项目依赖文件
└── .gitignore                                       # Git 忽略配置

三、核心代码流程

本项目采用 pytest + playwright + POM 的方式组织 UI 自动化测试代码。整体执行流程为:通过conftest.py 统一初始化浏览器和页面上下文,借助 storage_state 复用登录态;在 pages 目录中封装页面元素和业务操作;在 testcases 目录中编写测试用例;通过 JSON 文件维护测试数据,并结合 Allure、日志和截图记录测试执行结果。

3.1 全局前置配置

在自动化测试中,浏览器启动、页面上下文创建、登录态复用等操作属于公共前置条件。如果每个用例都重复编写这些代码,会导致脚本冗余且维护成本较高。因此,本项目通过 conftest.py 统一管理全局 fixture

@pytest.fixture(scope="session")
def browser():
    with sync_playwright() as p:
        browser = p.chromium.launch(
            headless=False,
            args=["--start-maximized"]
        )
        yield browser
        browser.close()

fixture 在整个测试会话中只启动一次浏览器,避免每条用例重复创建浏览器实例,提高测试执行效率。

3.2 登录态复用流程

对于需要登录后才能访问的功能模块,例如 AI 对话、文件上传、会话管理等,如果每条用例都重新执行登录操作,会降低执行效率。因此,本项目使用 Playwright storage_state 保存登录状态,并在后续用例中直接复用。

@pytest.fixture(scope="session")
def auth_state(browser):
    if AUTH_FILE.exists():
        return str(AUTH_FILE)

    context = browser.new_context(
        no_viewport=True,
        base_url=BASE_URL
    )

    page = context.new_page()
    login = LoginPage(page)
    login.login("账号", "密码", url="/auth")

    expect(page.locator("#chat-input")).to_be_visible(timeout=10000)

    context.storage_state(path=str(AUTH_FILE))
    context.close()

    return str(AUTH_FILE)

首次执行时,框架会完成登录并生成 auth_state.json 文件;后续执行时,如果检测到该文件存在,则直接复用登录态,减少重复登录操作。

@pytest.fixture(scope="function")
def logged_in_page(browser, auth_state):
    context = browser.new_context(
        no_viewport=True,
        base_url=BASE_URL,
        storage_state=auth_state
    )

    page = context.new_page()
    page.goto("/")

    yield page

    context.close()

通过该方式,每个测试用例都可以直接获取已登录页面对象,从而专注于业务功能验证。

3.3 POM 页面对象封装

本项目采用 POM 页面对象模式,将页面元素定位和业务操作封装到 pages 目录中,测试用例只需要调用对应方法,不直接操作复杂的页面定位语句。

以登录页面为例:

class LoginPage:
    def __init__(self, page):
        self.page = page
        self.email_input = page.locator("#email")
        self.password_input = page.get_by_placeholder("输入您的密码")
        self.login_button = page.get_by_text("登录", exact=True)

    def navigate(self, url):
        self.page.goto(url)

    def input_email(self, email):
        self.email_input.fill(email)

    def input_password(self, password):
        self.password_input.fill(password)

    def click_login_button(self):
        self.login_button.click()

    def login(self, email, password, url="/auth"):
        self.navigate(url)
        self.input_email(email)
        self.input_password(password)
        self.click_login_button()

通过这种方式,页面元素定位与测试逻辑实现了解耦。当页面元素发生变化时,只需要修改页面对象层的定位方式,不需要大范围修改测试用例。

3.4 测试数据参数化

为了避免将测试数据直接写死在用例中,项目将登录成功、登录失败、AI 对话等数据存放在 data 目录下的 JSON 文件中,并通过公共方法统一读取。

def get_json_data(file_path):
    file_path = BASE_DIR / file_path

    with open(file_path, "r", encoding="utf-8") as f:
        data = json.load(f)

    data_list = []
    for item in data:
        data_list.append(tuple(item.values()))

    return data_list

在测试用例中,结合 @pytest.mark.parametrize 参数化装饰器,实现多组数据的批量执行。

@pytest.mark.parametrize(
    "email, password, expected",
    get_json_data("data/login_fail_data.json")
)
def test_login_fail(login_page, email, password, expected):
    login_page.login(email, password)
    expect(login_page.page.get_by_text(expected)).to_be_visible()

这种方式可以有效实现测试数据与测试逻辑分离,提高用例扩展能力。

3.5 AI 问答核心流程

AI 问答模块是本项目的核心业务场景,主要验证模型选择、问题输入、消息发送、回复生成等流程。

class AIQuestionAnsweringProcessPage:
    def __init__(self, page):
        self.page = page
        self.model_select = page.locator("button[aria-haspopup='listbox']")
        self.qwen_model = page.get_by_role("option", name="选择模型")
        self.chat_input = page.locator("#chat-input > [data-placeholder='有什么我能帮您的吗?']")
        self.send_button = page.locator("#send-message-button")
        self.message_items = page.locator("div[role='listitem']")

    def user_chat_process(self, text):
        expect(self.chat_input).to_be_visible(timeout=10000)
        self.model_select.click()
        self.qwen_model.click()
        self.chat_input.fill(text)
        self.send_button.click()

在测试用例中,调用页面对象封装的方法完成用户提问,并通过 Playwright 的等待和断言机制校验 AI 回复内容是否正常生成。

def test_ai_chat_process(logged_in_page):
    ai_page = AIQuestionAnsweringProcessPage(logged_in_page)
    ai_page.user_chat_process("请介绍一下 Playwright")

    expect(ai_page.message_items.last).to_be_visible(timeout=60000)

由于 AI 回复存在一定的不确定性,因此用例中重点校验回复是否生成、页面状态是否变化,而不是固定断言某一段完全一致的回复内容。

3.6 断言机制设计

在 UI 自动化测试中,断言是判断用例是否执行成功的关键。本项目主要结合 Playwright 的 expect 断言机制Python 原生 assert 断言,分别对页面元素状态、业务流程结果、接口请求和接口响应进行校验。

首先,在页面功能验证中,主要使用 expect() 判断页面元素是否符合预期状态。例如登录成功后,判断聊天输入框是否可见;AI 回复生成后,判断回复消息是否出现在页面中;文件上传后,判断上传结果或相关页面元素是否展示。

expect(page.locator("#chat-input")).to_be_visible(timeout=10000)

该断言用于校验登录成功后是否进入 AI 对话页面。如果指定时间内聊天输入框可见,则说明登录流程正常;否则用例执行失败。

在 AI 对话场景中,由于模型回复内容具有一定不确定性,因此不适合直接断言完整回复文本。本项目主要从页面状态和元素变化角度进行校验,例如判断回复区域是否出现、发送按钮和停止按钮状态是否变化、消息列表是否新增内容等。

expect(ai_page.message_items.last).to_be_visible(timeout=60000)

这种断言方式可以避免由于 AI 回复内容不固定导致用例不稳定,更适合智能问答类平台的自动化测试。

其次,在表单校验场景中,通过断言错误提示文本是否出现,验证前端输入校验逻辑是否正确。例如邮箱为空、邮箱格式错误、密码为空、密码规则不符合要求等场景,都可以通过文本断言完成校验。

expect(login_page.email_empty_error).to_be_visible()
expect(login_page.password_error).to_be_visible()

通过这种方式,可以验证页面是否根据不同输入场景给出正确提示。

此外,在接口链路校验中,项目使用 Python 原生 assert Ajax 请求和响应结果进行断言。例如,在登录接口请求中,断言请求方法是否为 POST、请求头是否为 JSON、请求体中的邮箱和密码是否正确传递。

assert req.value.method == "POST"
assert "application/json" in req.value.header_value("content-type")
assert req.value.post_data_json == {
    "email": "admin@163.com",
    "password": "AaAa123456"
}

对于接口响应,则主要断言响应状态码是否符合预期。

assert res.value.status == 200

通过页面断言和接口断言结合的方式,本项目不仅能够验证用户操作后的页面表现,还能进一步确认接口请求是否正确发送、响应结果是否正常返回,从而实现 UI 操作与接口链路的联合校验。

最后,在异常场景中,通过 Mock 接口异常响应,并断言前端是否展示对应错误提示,验证系统的异常处理能力。

expect(page.get_by_text("Internal Server Error")).to_be_visible(timeout=5000)

整体来看,本项目的断言设计主要包括三类:页面状态断言、业务结果断言和接口链路断言。通过多层断言机制,可以提高自动化测试结果的准确性,也方便在用例失败时快速定位问题。

3.7 Ajax 请求与响应断言

除了页面操作校验外,本项目还结合 Playwright 的网络监听能力,对登录接口的请求和响应进行断言,实现 UI 操作与接口链路的联合校验。

def test_login_request(login_page):
    login_page.navigate("/auth")
    login_page.input_email("账号")
    login_page.input_password("密码")

    with login_page.page.expect_request("**/api/v1/auths/signin") as req:
        login_page.click_login_button()

    assert req.value.method == "POST"
    assert "application/json" in req.value.header_value("content-type")

响应结果断言如下:

def test_login_response(login_page):
    login_page.navigate("/auth")
    login_page.input_email("账号")
    login_page.input_password("密码")

    with login_page.page.expect_response("**/api/v1/auths/signin") as res:
        login_page.click_login_button()

    assert res.value.status == 200

通过这种方式,不仅可以验证页面是否操作成功,还可以进一步确认接口请求是否正常发送、响应状态是否符合预期。

3.8 Mock 异常场景验证

为了覆盖服务端异常场景,项目使用 Playwright 的 route.fulfill 对接口响应进行 Mock,模拟登录接口返回 500、502 等异常状态,验证前端是否能够正确展示错误提示。

def handle_500(route):
    route.fulfill(
        status=500,
        content_type="application/json",
        body='{"detail":"Internal Server Error"}'
    )

测试用例如下:

def test_login_api_500(login_page):
    page = login_page.page

    login_page.navigate("/auth")
    login_page.input_email("账号")
    login_page.input_password("密码")

    page.route("**/api/v1/auths/signin", handler=handle_500)

    login_page.click_login_button()

    expect(page.get_by_text("Internal Server Error")).to_be_visible(timeout=5000)

通过 Mock 异常响应,可以在不依赖真实服务端异常的情况下,主动构造异常场景,提高测试覆盖率。

3.9 日志与测试结果记录

项目封装了统一的日志模块,用于记录用例执行过程中的关键信息。同时结合 Pytest Hook、截图机制和 Allure 报告,对失败用例进行结果留存,方便后续问题定位。

logs.info("开始执行登录用例")
logs.info("登录接口响应状态断言成功")

当用例执行失败时,框架会自动保存失败截图,并将测试结果输出到 test_results 和 allure-results 目录中。后续可通过 Allure 命令生成可视化测试报告。

pytest --alluredir=allure-results
allure serve allure-results

4.0 失败日志与截图 Hook 机制

在自动化测试执行过程中,如果用例失败,仅依靠控制台报错信息往往不方便定位问题。因此,本项目基于 Pytest Hook 机制封装了失败自动处理逻辑,当用例执行失败时,自动记录失败日志,并保存当前页面截图,方便后续排查问题。

本项目主要使用 pytest_runtest_makereport 钩子函数获取用例执行结果。该钩子函数会在用例执行过程中生成测试报告对象,通过判断 report.failed report.passed 可以区分用例执行状态。

@pytest.hookimpl(hookwrapper=True, tryfirst=True)
def pytest_runtest_makereport(item, call):
    """
    用例失败时,自动记录日志并截图
    """
    outcome = yield
    report = outcome.get_result()

    if report.when == "call":
        if report.failed:
            logs.error(f"用例执行失败:{item.nodeid}")
            logs.error(f"失败详情:\n{report.longrepr}")

            page = (
                item.funcargs.get("before_all")
                or item.funcargs.get("logged_in_page")
            )

            if page:
                screenshot_dir = Path("test_results/fail_screenshots")
                screenshot_dir.mkdir(parents=True, exist_ok=True)

                current_time = datetime.now().strftime("%Y%m%d_%H%M%S")
                screenshot_path = screenshot_dir / f"fail_{current_time}.png"

                page.screenshot(path=str(screenshot_path), full_page=True)

                logs.error(f"失败截图已保存:{screenshot_path}")

        elif report.passed:
            logs.info(f"用例执行通过:{item.nodeid}")

在这段代码中,report.when == "call" 表示只处理测试用例真正执行阶段的结果,避免在 setup 或 teardown 阶段重复记录。
当用例失败时,框架会通过 logs.error() 记录失败用例的名称和错误详情,同时从 fixture 中获取当前页面对象,并调用 Playwright 的 page.screenshot() 方法保存失败截图。

其中,截图统一保存到:

test_results/fail_screenshots/

通过这种方式,当用例执行失败时,可以同时查看日志文件和页面截图,快速判断失败原因是页面元素未加载、断言失败、接口异常,还是业务流程未按预期执行。

该机制提升了自动化测试框架的问题定位能力,也让测试结果更加可追踪。

4.1 自动读取用例注释并生成 Allure 标题

为了提高 Allure 测试报告的可读性,如果每个用例都手动编写 @allure.title() 和 @allure.feature(),会增加代码重复度。因此,本项目基于 Pytest Hook 机制,在用例执行时自动读取测试类和测试方法的文档字符串,并动态写入 Allure 报告中。

在 conftest.py 中,通过 pytest_runtest_call 钩子函数获取当前执行的测试用例对象。测试类的文档字符串会被设置为 Allure 的 feature,测试方法的文档字符串会被设置为 Allure 的 title。

def pytest_runtest_call(item: Item):
    """
    动态添加 allure feature/title
    """
    if item.parent and getattr(item.parent, "_obj", None):
        if item.parent._obj.__doc__:
            allure.dynamic.feature(item.parent._obj.__doc__)

    if item.function.__doc__:
        allure.dynamic.title(item.function.__doc__)

例如,在测试用例中可以这样编写说明信息:

class TestLogin:
    """登录模块测试"""

    def test_01_login_success(self, login_page):
        """
        测试登录成功
        email: 已注册邮箱
        password: 正确密码
        """
        login_page.navigate(url="/auth")
        login_page.input_email("admin@163.com")
        login_page.input_password("AaAa123456")
        login_page.click_login_button()

当用例执行时,框架会自动读取:

测试类文档字符串  ->  Allure feature
测试方法文档字符串 ->  Allure title

这样可以减少重复的 Allure 装饰器代码,使测试用例本身更加简洁。同时,测试报告中的模块名称和用例标题会根据代码中的说明信息自动生成,提升报告的可读性和维护效率。

需要注意的是,这里读取的是 Python 的文档字符串,也就是类或函数下方使用三引号编写的内容,而不是普通的 # 单行注释。

4.2 核心执行流程总结

本项目的核心代码执行流程可以概括为:

读取 pytest 配置
        ↓
执行 conftest.py 中的 fixture
        ↓
启动浏览器并创建页面上下文
        ↓
判断是否存在 storage_state 登录态文件
        ↓
存在则直接复用登录态,不存在则登录后保存
        ↓
调用 pages 页面对象层封装方法
        ↓
执行 testcases 中的测试用例
        ↓
读取 data 中的 JSON 测试数据
        ↓
通过 Playwright 进行页面操作、等待和断言
        ↓
结合 Ajax 监听和 Mock 完成接口链路校验
        ↓
生成日志、截图和 Allure 测试报告

通过以上流程,框架实现了测试数据、页面操作、用例逻辑和测试结果的分层管理,提高了 UI 自动化测试脚本的可维护性、稳定性和扩展性。

四、GitHub 与 Jenkins 持续集成流程

本地开发并调试自动化测试脚本
        ↓
通过 Git 提交代码到本地仓库
        ↓
将代码推送到 GitHub 远程仓库
        ↓
Jenkins 拉取 GitHub 仓库代码
        ↓
安装项目依赖和 Playwright 浏览器环境
        ↓
执行 pytest 自动化测试命令
        ↓
生成 allure-results 原始测试结果
        ↓
Jenkins 调用 Allure 插件生成测试报告
        ↓
归档日志、截图和测试报告
        ↓
查看构建结果并定位失败用例

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐