基于 Playwright + Pytest 的 Web自动化测试框架搭建

📅 2026/8/12 19:35:44
基于 Playwright + Pytest 的 Web自动化测试框架搭建
项目介绍网址http://47.116.12.183/项目简介本项目基于Python编程语言采用Playwright作为核心浏览器自动化驱动库结合Pytest测试框架管理用例执行与断言并集成Allure生成测试报告。语言python库playwright测试框架pytest报告工具allure核心目标覆盖 Web 平台的关键业务流程并且让回归测试稳定、可追溯、可扩展。项目架构playwrightpytest-ui │ ├── cases # 测试用例层 ├── pages # POM页面对象层封装页面元素和页面行为 ├── mocks # 接口mock模拟接口异常响应 ├── plugins # pytest插件扩展 ├── auth # 登录状态管理 ├── report/reports # 测试报告 ├── test-results # trace、截图、视频 ├── conftest.py # fixture管理 ├── pytest.ini # 测试配置 └── run.py # 自动执行入口测试覆盖类型测试类型覆盖内容业务流程测试用户认证、项目管理、模块管理、环境管理接口异常测试400、403、500模拟权限测试无权限删除状态管理测试登录态保持、多账号隔离项目实战项目所需依赖包pip install playwright playwright install pip install pytest-playwright pip install pytest-base-url pip install allure-pytest安装完成后通过 pip freeze 命令自动生成 requirements.txt 文件部分测试用例注册页面用例用例编号所属模块用例名称优先级前置条件测试数据测试步骤预期结果REGISTER-001用户注册新用户注册成功P0已进入注册页面生成的随机用户名未被注册用户名随机UUID前8位密码aa1234561. 生成随机用户名2. 输入用户名3. 输入合法密码4. 点击注册按钮注册成功页面标题为“首页”URL跳转到/index.htmlREGISTER-002用户注册用户名为空注册校验P1已进入注册页面用户未登录用户名空密码1234561. 用户名不输入2. 输入密码3. 点击注册按钮显示用户名不能为空提示提示内容包含“不能为空”REGISTER-003用户注册用户名超过30字符校验P2已进入注册页面用户未登录用户名hello world hello world hello world1. 输入超过30字符的用户名2. 查看用户名提示信息3. 查看注册按钮状态显示“用户名称1-30位字符”提示注册按钮不可点击REGISTER-004用户注册用户名包含特殊字符校验P2已进入注册页面用户未登录用户名hello!#1. 输入包含特殊字符的用户名2. 查看用户名提示信息3. 查看注册按钮状态显示“用户名称不能有特殊字符,请用中英文数字”提示注册按钮不可点击REGISTER-005用户注册密码为空注册校验P1已进入注册页面用户未登录用户名hello密码空1. 输入合法用户名2. 密码不输入3. 点击注册按钮显示密码不能为空提示提示内容包含“不能为空”REGISTER-006用户注册密码少于6位校验P2已进入注册页面用户未登录密码abc121. 输入5位密码2. 查看密码校验提示显示“密码6-16位字符”提示REGISTER-007用户注册密码超过16位校验P2已进入注册页面用户未登录密码abc1234567890abc11. 输入超过16位密码2. 查看密码校验提示显示“密码6-16位字符”提示REGISTER-008用户注册密码包含特殊字符校验P2已进入注册页面用户未登录密码abc123!1. 输入包含特殊字符的密码2. 查看密码校验提示显示“不能有特殊字符,请用中英文数字下划线”提示REGISTER-009用户注册已有账号跳转登录页面P2已进入注册页面用户未登录无1. 检查“已有账号点这登录”链接的href属性2. 点击登录链接链接href为login.html成功跳转到登录页页面标题为“网站登录”REGISTER-010用户注册已注册账号重复注册P1已进入注册页面账号py已存在用户名py密码1234561. 输入已注册用户名2. 输入密码3. 点击注册按钮注册失败页面显示注册错误提示生成随机账号依靠 Python 自带的uuid模块代码如下def test_register_success(self): 测试注册正常流程注册成功 import uuid self.register.fill_username(str(uuid.uuid4())[:8]) self.register.fill_password(aa123456) self.register.click_register_button() expect(self.register.page).to_have_title(首页) expect(self.register.page).to_have_url(/index.html)其中uuid.uuid4()就是生成一个随机 UUID通过str()讲其转换成字符串完整 UUID 通常有 36 个字符因此str(uuid.uuid4())[:8]使用了 Python 的字符串切片取前 8 个字符既满足用户名长度限制又可以降低测试账号重复的概率提高用例的可重复执行性。登录页面用例用例编号所属模块用例名称优先级前置条件测试数据测试步骤预期结果LOGIN-001用户登录正确用户名密码登录成功P0已进入登录页面用户未登录账号有效用户名py密码1234561. 输入正确用户名2. 输入正确密码3. 点击登录按钮登录成功页面标题为“首页”URL跳转到/index.htmlLOGIN-002用户登录用户名为空登录校验P1已进入登录页面用户未登录用户名空密码1234561. 用户名不输入2. 输入密码3. 点击登录按钮显示用户名不能为空提示提示内容包含“不能为空”LOGIN-003用户登录用户名超过30字符校验P2已进入登录页面用户未登录用户名hello world hello world hello world1. 输入超过30字符的用户名2. 查看用户名校验提示及登录按钮状态显示“用户名称1-30位字符”提示登录按钮不可点击LOGIN-004用户登录用户名包含特殊字符校验P2已进入登录页面用户未登录用户名hello!#1. 输入带特殊字符的用户名2. 查看用户名校验提示及登录按钮状态显示“用户名称不能有特殊字符,请用中英文数字”提示登录按钮不可点击LOGIN-005用户登录正确账号错误密码登录失败P1已进入登录页面用户未登录用户名py密码123456781. 输入正确用户名2. 输入错误密码3. 点击登录按钮登录失败页面显示登录错误提示LOGIN-006用户登录错误账号错误密码登录失败P1已进入登录页面用户未登录用户名py11密码123456781. 输入错误用户名2. 输入错误密码3. 点击登录按钮登录失败页面显示登录错误提示LOGIN-007用户登录登录成功页面跳转校验P1已进入登录页面用户未登录账号有效用户名py密码1234561. 输入正确用户名和密码2. 监听页面导航3. 点击登录按钮浏览器成功导航到**/index.htmlLOGIN-008用户登录登录接口请求参数校验P1已进入登录页面用户未登录网络请求正常用户名py密码1234561. 输入正确用户名和密码2. 监听/api/login请求3. 点击登录按钮4. 获取请求方法、请求头及请求体请求方式为 POSTContent-Type为application/json请求体为{username:py,password:123456}LOGIN-009用户登录登录接口响应状态校验P1已进入登录页面用户未登录登录接口正常用户名py密码1234561. 输入正确用户名和密码2. 监听/api/login响应3. 点击登录按钮4. 获取响应状态登录接口响应成功okTrueHTTP状态码为200LOGIN-010用户登录注册链接跳转校验P2已进入登录页面用户未登录无1. 检查“没有账号点这注册”链接的href属性2. 点击注册链接链接href为register.html跳转到/register.html页面标题为“注册”关于用户登录页面测试中的异步请求和异步响应的断言目的是不同层次验证了登录功能。该测试本质上是页面点击登录按钮后浏览器会异步发送 AJAX 请求而 Playwright 通过expect_request()/expect_response()先监听再等待对应请求或响应出现然后再做断言。代码如下def test_login_ajax(self): 登录正常流程 获取异步ajax 请求 self.login.fill_username(py) self.login.fill_password(123456) with self.login.page.expect_request(**/api/login) as req: self.login.click_login_button() print(req.value) print(req.value.method) print(req.value.header_value) print(req.value.post_data_json) assert req.value.method POST assert req.value.header_value(content-type) application/json assert req.value.post_data_json { username: py, password: 123456 } def test_login_ajax_response(self): 登录正常流程 获取异步ajax 响应 self.login.fill_username(py) self.login.fill_password(123456) with self.login.page.expect_response(**/api/login) as res: self.login.click_login_button() print(res.value) print(res.value.url) print(res.value.status) print(res.value.ok) assert res.value.ok assert res.value.status 200新增项目用例用例编号所属模块用例名称优先级前置条件测试数据测试步骤预期结果PROJECT-001添加项目添加项目成功并跳转列表页P0用户已登录并进入添加项目页面项目名称不存在项目名称UUID生成的随机25位字符串所属应用xx项目描述xxx1. 动态生成随机项目名称2. 输入所属应用和项目描述3. 点击保存4. 监听页面导航项目创建成功并跳转到list_project.html项目列表页PROJECT-002添加项目添加成功后校验项目出现在列表P0用户已登录并进入添加项目页面项目名称不存在项目名称UUID随机25位字符串所属应用xx项目描述xxx1. 生成随机项目名称2. 填写项目信息3. 点击保存并等待页面跳转4. 校验URL5. 在列表中查找新增项目名称成功进入list_project.html项目列表中能够找到刚新增的项目名称PROJECT-003添加项目项目名称为空P1用户已登录并进入添加项目页面项目名称空所属应用空项目描述空1. 项目名称不输入2. 所属应用和描述留空3. 点击保存按钮4. 查看保存按钮状态项目名称为空时不能正常提交保存按钮处于不可用状态PROJECT-004添加项目项目名称重复时400异常处理P1用户已登录并进入添加项目页面已配置 Mock 将接口返回400项目名称yo yo所属应用xx项目描述xxx接口响应4001. 使用page.route()Mock 接口返回4002. 输入项目信息3. 点击保存4. 查看页面反馈前端正确处理400异常并显示错误模态框PROJECT-005添加项目服务器500异常处理P1用户已登录并进入添加项目页面已配置 Mock 将接口返回500用户已登录并进入添加项目页面项目名称test所属应用xx项目描述xxx接口响应500项目名称abc!所属应用空项目描述空1. 使用page.route()Mock 接口返回5002. 输入项目信息3. 点击保存4. 查看错误提示1. 输入项目名称abc!2. 所属应用留空3. 项目描述留空4. 查看保存按钮状态页面弹出异常提示框提示内容包含“操作异常”保存按钮不可点击PROJECT-006添加项目项目名称包含特殊字符P2用户已登录并进入添加项目页面项目名称abc!所属应用空项目描述空1. 输入项目名称abc!2. 所属应用留空3. 项目描述留空4. 查看保存按钮状态保存按钮不可点击PROJECT-007添加项目所属应用包含特殊字符P2用户已登录并进入添加项目页面项目名称abc所属应用aa!项目描述空1. 输入合法项目名称2. 输入包含特殊字符的所属应用3. 项目描述留空4. 查看保存按钮状态保存按钮不可点击PROJECT-008添加项目项目名称超过30字符P2用户已登录并进入添加项目页面项目名称aaaaabbbbbcccccdddddeeeeefffff1所属应用空项目描述空1. 输入超过30字符的项目名称2. 所属应用留空3. 项目描述留空4. 查看保存按钮状态保存按钮不可点击PROJECT-009添加项目所属应用超过30字符P2用户已登录并进入添加项目页面项目名称abc所属应用aaaaabbbbbcccccdddddeeeeeffffff项目描述空1. 输入合法项目名称2. 输入超过30字符的所属应用3. 项目描述留空4. 查看保存按钮状态保存按钮不可点击PROJECT-010添加项目项目描述超过200字符P2用户已登录并进入添加项目页面项目名称abc所属应用空项目描述201个字符a1. 输入合法项目名称2. 所属应用留空3. 输入201个字符的项目描述4. 查看保存按钮状态保存按钮不可点击成功新增项目用例代码如下def test_add_project_success(self, page: Page): 提交成功跳转到项目列表 self.add_project.fill_project_name( str(uuid.uuid4()).replace(-, )[:25] ) self.add_project.fill_publish_app(xx) self.add_project.fill_project_desc(xxx) with page.expect_navigation(url**/list_project.html): self.add_project.click_save_button() def test_add_project_success2(self, page: Page): 提交成功判断新项目名称在列表 new_project_name ( str(uuid.uuid4()).replace(-, )[:25] ) self.add_project.fill_project_name(new_project_name) self.add_project.fill_publish_app(xx) self.add_project.fill_project_desc(xxx) with self.add_project.page.expect_navigation(): self.add_project.click_save_button() import re expect(self.add_project.page).to_have_url( re.compile(.*/list_project.html) ) project self.add_project.page.locator( //table[idtable]//td[3]/a, has_textnew_project_name ) expect(project).to_be_visible()test_add_project_success主要通过expect_navigation()验证新增成功后的页面跳转test_add_project_success2除了验证跳转外还通过 XPath 定位项目列表第三列并结合has_text精确查找随机生成的项目名称最后使用to_be_visible()验证新增数据确实展示在列表中。test_add_project_success2用例有两个断言:第一个断言使用正则表达式re.compile(.*/list_project.html)验证页面是否进入项目列表页。with self.add_project.page.expect_navigation(): self.add_project.click_save_button() import re expect(self.add_project.page).to_have_url( re.compile(.*/list_project.html) )等价于with page.expect_navigation(url**/list_project.html): self.add_project.click_save_button()第二个断言project self.add_project.page.locator( //table[idtable]//td[3]/a, has_textnew_project_name )是在定位刚刚新增的项目。//table[idtable]//td[3]/a 是Xpath作用为找到 id 为 table 的项目列表表格 → 找到其中第三列 → 找到第三列中的 a 标签。同时利用 has_textnew_project_name 筛选出刚刚创建的项目。Mock测试Mock 测试主要用于模拟真实环境中不容易稳定触发的接口响应比如 400、500 等异常状态码。通过 Playwright 的page.route()拦截请求并返回指定响应可以减少对后端服务和数据库数据的依赖提高自动化用例的稳定性和可重复执行性同时验证前端对异常响应的处理逻辑。代码如下def test_add_project_400(self, page: Page): 模拟 项目名称重复弹出模态框 400 状态码 self.add_project.page.route( **mock_api.mock_project_400 ) self.add_project.fill_project_name(yo yo) self.add_project.fill_publish_app(xx) self.add_project.fill_project_desc(xxx) self.add_project.click_save_button() expect( self.add_project.locator_boot_box ).to_be_visible() def test_add_project_500(self, page: Page): 模拟 服务器异常 500 状态码 # mock 接口返回500 page.route(**mock_api.mock_project_500) self.add_project.fill_project_name(test) self.add_project.fill_publish_app(xx) self.add_project.fill_project_desc(xxx) self.add_project.click_save_button() # 校验结果 弹出框文本包含 expect(self.add_project.locator_boot_box).to_contain_text(操作异常)当项目名称已存在时实际HTTP响应状态码为400page.route()是 Playwright 的网络拦截功能,mock_api.mock_project_400和mock_api.mock_project_500是我们人为提前配置好的 Mock 数据。真正测试的是如果后端真的出现这些异常前端页面能不能正确处理。项目列表用例项目列表页面强依赖后端返回的数据为了稳定构造0条、1条、指定项目、403、400、500等状态通过 Mock 固定接口响应减少数据库数据和后端状态对前端自动化测试的影响。测试分成四部分新增项目弹窗 → 项目名称空值、取消新增项目、项目名称重复 400、服务器异常 500、新增成功 200 项目搜索 → 点击搜索按钮、搜索名称不存在 搜索结果为0条、搜索名称存在 搜索结果为1条 列表刷新 → GET请求及查询参数 表格行操作 → 链接、删除、编辑用例编号所属模块用例名称优先级前置条件测试数据测试步骤预期结果LIST-001项目列表-新增项目项目名称为空P1用户已登录并进入项目列表页项目名称空1. 点击新增项目2. 项目名称留空3. 点击保存新增项目模态框不关闭不能正常提交LIST-002项目列表-新增项目取消新增项目P2用户已登录并进入项目列表页项目名称空1. 点击新增项目2. 点击取消按钮新增项目模态框关闭LIST-003项目列表-新增项目项目名称重复400异常处理P1用户已登录并进入项目列表页已配置400 Mock项目名称yo yoMock响应4001. 打开新增项目模态框2. 输入项目名称3. Mock新增接口返回4004. 点击保存页面显示错误弹窗内容包含“已存在”LIST-004项目列表-新增项目服务器500异常处理P1用户已登录并进入项目列表页已配置500 Mock项目名称yo yoMock响应5001. 打开新增项目模态框2. 输入项目名称3. Mock接口返回5004. 点击保存页面显示错误提示内容包含“操作异常”LIST-005项目列表-新增项目新增项目成功P0用户已登录并进入项目列表页已配置200 Mock项目名称yo yoMock响应2001. 打开新增项目模态框2. 输入项目名称3. Mock接口返回2004. 点击保存新增成功后模态框关闭LIST-006项目列表-搜索搜索请求参数校验P1用户已登录并进入项目列表页搜索内容test1. 输入搜索内容2. 监听项目查询请求3. 点击搜索发出GET请求URL参数中包含project_nametestLIST-007项目列表-搜索搜索结果为空P1用户已登录并进入项目列表页已配置空结果Mock搜索内容testMock返回0条数据1. 输入搜索条件2. 验证输入框值3. Mock搜索结果为空4. 点击搜索输入框内容为test表格显示“没有找到匹配的记录”LIST-008项目列表-搜索搜索返回一条结果P1用户已登录并进入项目列表页已配置1条结果Mock搜索内容testMock返回1条数据1. 输入搜索条件2. 验证输入框值3. Mock返回1条记录4. 点击搜索搜索结果表格显示1条记录LIST-009项目列表-刷新刷新列表请求校验P1用户已登录并进入项目列表页默认查询参数1. 监听项目列表查询请求2. 点击刷新按钮发出GET请求URL包含page1size15project_nameLIST-010项目列表-表格项目行内链接地址校验P2用户已登录Mock列表存在指定项目数据Mock项目ID28121. Mock一条列表数据2. 刷新页面3. 获取表格行内链接链接href为debugtalk.html?project_id2812LIST-011项目列表-表格删除项目无权限处理P1用户已登录Mock列表存在指定项目删除接口Mock为403删除接口返回无权限操作1. Mock列表数据2. 刷新页面3. 点击删除4. 验证确认弹窗5. Mock删除接口4036. 点击确定首先出现删除确认框删除后出现异常提示内容包含“无权限操作请联系管理员”LIST-012项目列表-表格编辑项目弹窗显示P1用户已登录Mock列表存在可编辑项目Mock一条项目数据1. Mock列表数据2. 刷新页面3. 点击编辑按钮编辑项目模态框可见解决反复登录问题登录一次后面的用例复用登录状态避免每个用例都重新登录而登录/注册测试仍然保持未登录状态。/conftest.pyimport pytest from pages.login_page import LoginPage # 1. 整个测试会话只登录一次并保存登录状态 pytest.fixture(scopesession) def login_first(browser, base_url, pytestconfig): 登录一次并保存登录状态 context browser.new_context( base_urlbase_url, no_viewportTrue ) page context.new_page() login LoginPage(page) login.navigate() login.login(py, 123456) # 等待登录成功 page.wait_for_url(**/index.html) # 保存 Cookie / LocalStorage 等登录状态 storage_path pytestconfig.rootpath.joinpath(auth/login_first.json) context.storage_state(pathstorage_path) context.close() return storage_path # 2. 修改 pytest-playwright 默认 Context # 以后直接使用 page fixture 时自动加载登录状态 pytest.fixture(scopesession) def browser_context_args(browser_context_args, login_first): return { **browser_context_args, storage_state: login_first } # 3. 登录/注册测试专用创建一个不加载登录状态的 Context pytest.fixture(scopemodule) def unlogin_context(browser, base_url): context browser.new_context( base_urlbase_url, no_viewportTrue ) yield context context.close() # 4. 登录/注册测试使用的未登录页面 pytest.fixture def unlogin_page(unlogin_context): page unlogin_context.new_page() yield page page.close()多账号测试多账号测试主要通过 Playwright 的不同BrowserContext实现账号隔离。默认的page属于普通用户登录后的上下文而admin_context是单独为 admin 用户创建并保存登录状态的上下文再通过admin_context.new_page()创建 admin 的页面。由于不同 BrowserContext 的 Cookie、LocalStorage 和 Session 相互隔离因此两个 Page 可以同时保持不同账号的登录状态。测试中账号1负责创建项目admin账号在另一个 Context 中搜索并删除同一个项目从而实现多用户协作场景的自动化验证。more_account/conftest.pyimport pytest from pages.login_page import LoginPage 全局默认账号使用py/123456。在cases目录的conftest.py文件下 涉及多个账号切换操作的时候 我们可以创建新的上下文用其它账号登录 管理员账号admin/123456 pytest.fixture(scopemodule) def admin_context(browser, base_url, pytestconfig, browser_context_args): 创建admin上下文, 加载admin.json数据 :return: context browser.new_context(**browser_context_args) page context.new_page() LoginPage(page).navigate() LoginPage(page).login(admin, 123456) # 等待登录成功页面重定向 page.wait_for_url(url**/index.html) yield context context.close()测试代码 整个项目中可以用的上下文用3个 page 用例中直接传page默认使用登录后的context上下文创建的page对象 login_context 专门针对登录的用例创建的上下文与其他环境隔离 admin_context 针对admin用户登录后的上下文环境 from playwright.sync_api import BrowserContext, Page import pytest import uuid from pages.add_project_page import AddProjectPage from pages.project_list_page import ProjectListPage class TestMoreAccounts: pytest.fixture(autouseTrue) def start_for_each(self, page: Page, admin_context: BrowserContext): print(for each--start: 打开添加项目页) # 用户1 self.user1_project AddProjectPage(page) self.user1_project.navigate() # 用户2 page2 admin_context.new_page() self.user2_project ProjectListPage(page2) self.user2_project.navigate() yield print(for each--end: 后置操作) def test_delete_project(self): 测试流程 step--A账号登录创建项目xxx step--B账号登录删除项目xxx :return: # 账号 1 添加项目 test_project_name str(uuid.uuid4()).replace(-, )[:25] self.user1_project.fill_project_name(test_project_name) self.user1_project.fill_publish_app(xx) self.user1_project.fill_project_desc(xxx) # 断言跳转到项目列表页 with self.user1_project.page.expect_navigation(url**/list_project.html): # 保存成功后重定向到列表页 self.user1_project.click_save_button() # 账号 2 操作删除 self.user2_project.search_project(test_project_name) with self.user2_project.page.expect_request(**/api/project**): self.user2_project.click_search_button() self.user2_project.page.wait_for_timeout(3000) self.user2_project.locator_table_delete.click() # 确定删除 with self.user2_project.page.expect_response(**/api/project**) as resp: self.user2_project.locator_boot_box_accept.click() # 断言删除成功 resp_obj resp.value assert resp_obj.status 200allure测试报告下载allure报告生成器可以读取.json文件并生成HTML 网页报告。下载链接https://github.com/allure-framework/allure2/releases运行命令运行测试用例pytest --alluredir ./report查看报告allure serve ./report运行结果