该项目是一个基于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