基于playwright-ui的自动化框架搭建

该项目是一个基于playwright搭建的自动化框架,主要测试一个远程服务的平台,实现绝大部分UI自动化操作;

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

一、安装软件包

pip install allure-pytest
pip install allure-python-commons
pip install playwright
pip install pytest
pip install python-slugify
pip install requests

二、目录

cases :目录放测试用例

mocks: 目录放需要 mock 的数据

pages :放封装的 Page 页面对象

plugins :放第三方插件

allure_report:报告目录 ,自动生成

三、设计模式

本项目采用POM设计模式,先在pages文件夹中封装页面元素,再在cases文件夹中编写测试用例,达到解耦的效果;

POM(页面对象模型)

页面对象代表 Web 应用程序的一部分。应用程序可能有一个主页、一个列表页 面和一个结帐页面。它们中的每一个都可以由页面对象模型表示。 页面对象通过创建适合您的应用程序的更高级别的 API 来简化创作,并通过在一个地方捕获元 素选择器和创建可重用代码来避免重复来简化维护。

四、页面封装和用例设计(以Login页为例)

Login页面封装

在 pages 目录创建 login_page.py, 封装登录页面抓取元素的属性,以及页面上操作方法

from playwright.sync_api import Page
 class LoginPage:
 def __init__(self, page: Page):
 self.page = page
 self.locator_username = page.get_by_label("用 户 名:")
 self.locator_password = page.get_by_label("密     
码:")
 self.locator_login_btn = page.locator('text=立即登录')
 self.locator_register_link = page.locator('text=没有账号?点这注册')
 # 用户名输入框提示语
self.locator_username_tip1 = page\
 .locator('[data-fv-validator="notEmpty"][data-fv-for="username"]')
 self.locator_username_tip2 = page\
 .locator('[data-fv-validator="stringLength"][data-fv-for="username"]')
 self.locator_username_tip3 = page\
 .locator('[data-fv-validator="regexp"][data-fv-for="username"]')
 # 密码输入框提示语
self.locator_password_tip1 = page\
 .locator('[data-fv-validator="notEmpty"][data-fv-for="password"]')
 self.locator_password_tip2 = page\
 .locator('[data-fv-validator="stringLength"][data-fv-for="password"]')
 self.locator_password_tip3 = page\
 .locator('[data-fv-validator="regexp"][data-fv-for="password"]')
 # 账号或密码不正确!
self.locator_login_error = page\
 .locator('text=账号或密码不正确!')
 def navigate(self):
 self.page.goto("/login.html")
 def fill_username(self, username):
 self.locator_username.fill(username)
 def fill_password(self, password):
 self.locator_password.fill(password)
 def click_login_button(self):
 self.locator_login_btn.click()
 def click_register_link(self):
 self.locator_register_link.click()
 def login(self, username, password)-> None:
 """完整登录操作"""
self.locator_username.fill(username)
 self.locator_password.fill(password)
 self.locator_login_btn.click()

Login页的用例设计

用例的编写根据平常的功能测试用例去写

from pages.login_page import LoginPage
 from playwright.sync_api import expect
 import pytest
 import allure
 class TestLogin:
 """登录功能"""
 @pytest.fixture(autouse=True)
 def start_for_each(self, page):
 print("for each--start: 打开新页面访问登录页")
 self.login = LoginPage(page)
 self.login.navigate()
 yield
 print("for each--end: 后置操作")
 def test_login_1(self):
 """用户名为空,点注册"""
 self.login.fill_username('')
 self.login.fill_password('123456')
 self.login.click_login_button()
 # 断言
expect(self.login.locator_username_tip1)\
 .to_be_visible()
 expect(self.login.locator_username_tip1)\
 .to_contain_text("不能为空")
 def test_login_2(self):
 """用户名大于30字符"""
 self.login.fill_username('hello world hello world hello world')
 # 断言
expect(self.login.locator_username_tip2)\
 .to_be_visible()
 expect(self.login.locator_username_tip2)\
 .to_contain_text("用户名称1-30位字符")
 # 断言 登录按钮不可点击
expect(self.login.locator_login_btn).not_to_be_enabled()
 def test_login_3(self):
 """用户名有特殊字符"""
 self.login.fill_username('hello!@#')
 # 断言
expect(self.login.locator_username_tip3).to_be_visible()
 expect(self.login.locator_username_tip3)\
 .to_contain_text("用户名称不能有特殊字符")
# 断言 注册按钮不可点击
expect(self.login.locator_login_btn).not_to_be_enabled()
 @pytest.mark.parametrize("username,password,title",[
 ['yoyo', '12345678', '输入错误的密码'],
 ['yoyox1x2x3', '12345678', '输入错误的账号'],
 ])
 def test_login_error(self, username, password, title):
 """登录失败场景"""
 self.login.fill_username(username)
 self.login.fill_password(password)
 self.login.click_login_button()
 # 断言提示语可见
expect(self.login.locator_login_error).to_be_visible()
 def test_login_success(self):
 """登录正常流程, 登录成功"""
 self.login.fill_username("yoyo")
 self.login.fill_password('aa123456')
 self.login.click_login_button()
 # 断言页面跳转到首页
expect(self.login.page).to_have_title('首页')
 expect(self.login.page).to_have_url('/index.html')
 def test_login_success_2(self):
 """登录正常流程, 登录成功"""
 self.login.fill_username("yoyo")
 self.login.fill_password('aa123456')
 # 显示断言重定向
with self.login.page.expect_navigation(url='**/index.html'):
 self.login.click_login_button()
 # 断言页面跳转到首页
expect(self.login.page).to_have_title('首页')
 expect(self.login.page).to_have_url('/index.html')
 def test_login_ajax(self):
 """登录正常流程, 获取异步ajax 请求"""
 self.login.fill_username("yoyo")
 self.login.fill_password('aa123456')
 # 捕获ajax请求
with self.login.page.expect_request('**/api/login') as req:
 self.login.click_login_button()
 print(req.value) # 获取请求对象
# 断言请求内容
assert req.value.method == 'POST'
 assert req.value.header_value('content-type') == 'application/json'
assert req.value.post_data_json == {
 'username': 'yoyo', 'password': 'aa123456'
 }
 def test_login_ajax_response(self):
 """登录正常流程, 获取异步ajax 请求返回结果"""
 self.login.fill_username("yoyo")
 self.login.fill_password('aa123456')
 # 捕获ajax请求
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

五、如何实现断言(以登录成功为例)

当页面登录成功的时候,会重定向到首页,这时候我们如何断言登录成功了?

登录成功我们有多种判断方式:

1.页面跳转到首页,直接断言首页的 title 和 url 地址是否正确

2.显示断言 expect_navigation ,判断导航到指定 url 地址

3.也可以断言 Ajax 请求,判断前端发过去的 Ajax 请求参数是否正确

4.还可以断言 Ajax 响应数据,判断接口响应是否正确

页面跳转到首页,直接断言首页的 title 和 url 地址是否正确

def test_login_success(self):
 """登录正常流程, 登录成功"""
 self.login.fill_username("yoyo")
 self.login.fill_password('aa123456')
 self.login.click_login_button()
 # 断言页面跳转到首页
expect(self.login.page).to_have_title('首页')
 expect(self.login.page).to_have_url('/index.html')

显示断言你 expect_navigation ,判断导航到指定 url 地址

def test_login_success_2(self):
 """登录正常流程, 登录成功"""
 self.login.fill_username("yoyo")
 self.login.fill_password('aa123456')
 # 显示断言重定向
with self.login.page.expect_navigation(url='**/index.html'):
 self.login.click_login_button()
 # 断言页面跳转到首页
expect(self.login.page).to_have_title('首页')
 expect(self.login.page).to_have_url('/index.html')

六、Mock测试

如果你们项目是前后端分离的,你只关注前端功能(后端的功能给接口自动化人员去做) 那么我们不用管接口的返回内容,也不用跟数据库打交道。直接 mock 自己需要的数据即可。 有些调用第三方接口,也可以用 mock 拦截掉。

mock 数据 

import json
 """
 /**** 模拟新增项目 返回 400  ***/
 """
 mock_project_400 = {
 "url": "**/api/project",
 "handler": lambda route: route.fulfill(
 status=400,
 body=json.dumps({
 "errors":
 {
 "project_name": "yo yo 已存在"
 },
 "message": "Input payload validation failed"
 })
 )
 }
 """
 /**** 模拟新增项目 返回 500  ***/
 """
 mock_project_500 = {
 "url": "**/api/project",
 "handler": lambda route: route.fulfill(
 status=500,
 body="服务端错误"
 )
 }

mock用例场景

def test_add_project_400(self, page):
 """项目名称重复,弹出模态框"""
 self.add_project.fill_project_name("yo yo")
 self.add_project.fill_publish_app("xx")
 self.add_project.fill_project_desc("xxx")
 # mock 接口返回400
 page.route(**mock_api.mock_project_400)
 self.add_project.click_save_button()
 # 校验结果 弹出框文本包含
expect(page.locator('.bootbox-body')).to_contain_text('已存在')
 def test_add_project_500(self, page):
 """服务器异常 500 状态码"""
 self.add_project.fill_project_name("test")
 self.add_project.fill_publish_app("xx")
 self.add_project.fill_project_desc("xxx")
 # mock 接口返回500
 page.route(**mock_api.mock_project_500)
 self.add_project.click_save_button()
 # 校验结果 弹出框文本包含
expect(page.locator('.bootbox-body')).to_contain_text('操作异常')

七、生成Allure报告

自动生成 feature 和 title

为了避免每个用例都去加 @allure.feature(功能点) 和 @allure.title(用例的标题) 如下用例部分,我们可以根据 类的注释内容,自动生成 allure.feature(功能点) 测试用例的注释,自动生成 allure.title(用例的标题),测试用例的注释默认是 description (描 述)

from pytest import Item
 import allure
 def pytest_runtest_call(item: Item): # noqa
 # 动态添加测试类的 allure.feature()
 if item.parent._obj.__doc__: # noqa
 allure.dynamic.feature(item.parent._obj.__doc__) # noqa
 # 动态添加测试用例的title 标题 allure.title()
 if item.function.__doc__: # noqa
 allure.dynamic.title(item.function.__doc__) # noqa

添加用例步骤

class RegisterPage:
 def __init__(self, page: Page):
 self.page = page
 ......
 def navigate(self):
 with allure.step("导航到注册页"):
 self.page.goto("/register.html")
 def fill_username(self, username):
 with allure.step(f"输入用户名:{username}"):
 self.locator_username.fill(username)
 def fill_password(self, password):
 with allure.step(f"输入密码:{password}"):
 self.locator_password.fill(password)
 def click_register_button(self):
 with allure.step(f"点注册按钮"):
 self.locator_register_btn.click()
 def click_login_link(self):
 with allure.step(f"点登录链接"):
 self.locator_login_link.click()

报告展示

查看报告

allure serve ./report

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值