由于公司UI自动化框架底层用的是Uiautomator2,所以我就用Uiautomator2搭了一套UI自动化框架,并运用某软件做了一个实战,思路其实和之前写的Appnium一样的,有时间我会上传到github
ps:最近一直很忙,之前的Appnium搭建的企业微信实战,有时间我再把功能实现 0.0
-
github链接:
-
Appnium-企业微信UI自动化框架搭建:https://blog.csdn.net/Makasa/article/details/123963798?spm=1001.2014.3001.5502
一、Uiautomator2介绍
- 这篇文章写的很详细了大家可以学习学习,我就不再介绍了
链接:https://ceshiren.com/t/topic/5396
二、整体框架介绍
base包:封装基础类 case包:封装测试用例类 datas包:参数化数据(excel 或者 yml...) logs包:封装日志 page包:封装所有的页面 sms包:发送邮箱 report包:存放测试报告 utils包:工具包,例如读取参数化数据
三、框架搭建 Base包: 1、先创建一个BasePage.py
-
为什么要单独封装一个BasePage呢? 如果说以后我们不用uiautomator2这个框架了,我们只需要更改BasePage即可,不会影响到其他类的代码,从某种程度上来说也就是进行解耦
-
另外,这个类也可以封装自己写的公用的方法,例如:重复性很高的代码,这些方法不论在哪个app里都能用的话,我们就单独拧出来封装成一个方法,例如下我写的:关闭弹窗/重新传值等
"""
基类,大概率不变的东西放在基类
例如,框架本身的东西,或者自己封装的公用方法
"""
from datetime import time
from time import sleep
import uiautomator2 as u2
device = 'JJN6R18305005632'
class BasePage:
def __init__(self):
self.d = u2.connect(device)
def back(self, num):
# 返回
for i in range(num):
self.d.xpath("//*[@resource-id='com.zhenai.loveguider:id/iv_back']").click()
def clear_input_x_by_resourceId(self, location, value, x):
"""
清空已填写的选项,重新传值
:param location: 元素定位字符串
:param value: 重新传的值
:param x :索引
"""
self.d(resourceId=location)[x].click() # 定位到元素
self.d(focused=True).clear_text()
self.d.send_keys(value)
def clear_input_by_resourceId(self, location, value):
"""
清空已填写的选项,重新传值
:param location: 元素定位字符串
:param value: 重新传的值
"""
self.d(resourceId=location).click() # 定位到元素
self.d(focused=True).clear_text()
self.d.send_keys(value)
return self
def close_popup(self, text, locationType, location):
"""
关闭弹窗
:param text:字符串,弹窗包含的text文本
:param locationType:定位方式 xpath or resourceId
:param location:定位元素字符串,例:"//*[@text='登录']"
"""
element = self.d(text=text)
while True:
if element:
element.click()
continue
else:
print("权限校验弹窗不存在")
break
if locationType == "xpath":
self.d.xpath(location).click()
elif locationType == "resourceId":
self.d(resourceId=location).click()
def swipe_until_element_found(self, param, wait_after_found=0.0, **kwargs):
"""
检查元素是否存在,若不存在则进行上滑,滑动后再次检查,直到滑动到页面底部
若找到元素则返回,否则滑动到页面底部后,仍未找到元素,则抛出异常,提示找不到元素
:param param: xpath字符串 或 元素对象
:param wait_after_found: 找到元素后,原地等待时间
:param kwargs:
:return:
"""
element = self.d.xpath(param) if isinstance(param, str) else param
param = param if isinstance(param, str) else param.selector
while True:
try:
assert element.exists
if wait_after_found:
print("Element found,sleep {} seconds".format(wait_after_found))
sleep(wait_after_found)
return element
except AssertionError:
print("Element 【 {} 】 Not found, Continue to swipe up...".format(param))
# 获取滑动前页面下半部分的所有元素
page_content = self.d.dump_hierarchy()[(len(self.d.dump_hierarchy()) // 2):]
# self.up(**kwargs)
self.d.swipe_ext("up")
sleep(0.5)
# 获取滑动后页面下半部分的所有元素,并与上一次滑动前的页面元素对比,页面元素没有变化时跳出循环
if self.d.dump_hierarchy()[(len(self.d.dump_hierarchy()) // 2):] == page_content:
break
if not element.exists:
raise AssertionError("Element 【 {} 】 located failed in this page".format(param))
def swipe_for_click(self, param, wait_after_click=0.0, **kwargs):
"""
判断UI元素是否存在, 不存在则持续向上滑动到底部,直到UI元素在页面内出现,再进行点击
:param param: xpath字符串 或 元素对象
:param wait_after_click: 点击后等待时间
:return:
"""
element = self.swipe_until_element_found(param, **kwargs)
element.click()
if wait_after_click:
print("Element found and click,then sleep {} seconds".format(wait_after_click))
sleep(wait_after_click)
def up(self, scale=0.9, times=1, duration=1.0, **kwargs):
"""
上滑操作
:param scale: 滑动单位,默认0.9个单位
:param times: 滑动次数,默认1次
:param duration: 滑动时间,默认1.0秒
:return:
"""
for i in range(times):
self.d.swipe_ext("up", scale, duration=duration, **kwargs)
def down(self, scale=0.9, times=1, duration=1.0, **kwargs):
"""
下滑操作
:param scale: 滑动单位,默认0.9个单位
:param times: 滑动次数,默认1次
:param duration: 滑动时间,默认1.0秒
:return:
"""
for i in range(times):
self.d.swipe_ext("down", scale, duration=duration, **kwargs)
def left(self, scale=0.9, times=1, duration=1.0, **kwargs):
"""
左滑操作
:param scale: 滑动单位,默认0.9个单位
:param times: 滑动次数,默认1次
:param duration: 滑动时间,默认1.0秒
:return:
"""
for i in range(times):
self.d.swipe_ext("left", scale, duration=duration, **kwargs)
def right(self, scale=0.9, times=1, duration=1.0, **kwargs):
"""
右滑操作
:param scale: 滑动单位,默认0.9个单位
:param times: 滑动次数,默认1次
:param duration: 滑动时间,默认1.0秒
:return:
"""
for i in range(times):
self.d.swipe_ext("right", scale, duration=duration, **kwargs)
def wait_until_element_found(self, param, timeout=30.0, retry_interval=2, wait_after_found=0.0):
"""
定位元素,如果不存在就间隔若干秒后重试,直到元素定位成功或超时
:param param: xpath字符串 或 元素对象
:param timeout: 超时, 默认30秒
:param retry_interval: 间隔时间, 默认2秒
:param wait_after_found: 找到元素后,原地等待时间
:return:
"""
element = self.d.xpath(param) if isinstance(param, str) else param
max_time = time() + timeout
while True:
try:
assert element.exists
if wait_after_found:
print("Element found,then sleep {} seconds".format(wait_after_found))
sleep(wait_after_found)
return element
except AssertionError:
param = param if isinstance(param, str) else param.selector
print("Element 【 {} 】 Not found, Retry...".format(param))
if time() > max_time > 0:
raise AssertionError("Element 【 {} 】 located failed after {} timeout".format(param, timeout))
sleep(retry_interval)
def wait_for_click(self, param, wait_after_click=0.0, **kwargs):
"""
判断UI元素是否存在, 不存在则等待UI元素在一定时间内出现,再进行点击
:param param: xpath字符串 或 元素对象
:param wait_after_click: 点击后等待时间
:return:
"""
element = self.wait_until_element_found(param, **kwargs)
element.click()
if wait_after_click:
print("Element found and click,then sleep {} seconds".format(wait_after_click))
sleep(wait_after_click)
def repeat_click(self, param, times, wait_after_repeat_click=0.0):
"""
重复多次点击UI元素
:param param: xpath字符串 或 元素对象
:param times: 点击次数
:param wait_after_repeat_click: 重复点击后等待时间,默认为0.0
:return:
"""
element = self.wait_until_element_found(param)
for i in range(times):
element.click()
if wait_after_repeat_click:
print("Element click,then sleep {} seconds".format(wait_after_repeat_click))
sleep(wait_after_repeat_click)
2、创建一个LoverPadApp.py,继承BasePage
- 由于以后可能又去测试其他的App,所以我们这边就单独再创建一个LoverPadApp的类继承BasePage,为了区分App
"""
恋爱宝相关的操作
"""
from time import sleep
from mikasa_script.pad_ui_auto.base.BasePage import BasePage
class LoverPadApp(BasePage):
def start(self):
"""
启动App
"""
if self.d == None:
self.d.app_start('com.zhenai.loveguider')
return self.d
def restart(self):
"""
重启App
"""
pass
def clear(self):
"""
清除app缓存
"""
self.d.app_clear('com.zhenai.loveguider')
def stop(self):
"""
关闭App
"""
self.d.app_stop('com.zhenai.loveguider')
def login_goto_main(self, workerId, password):
"""
员工登录进首页
"""
from mikasa_script.pad_ui_auto.page.MainPage import MainPage
if self.d != None:
return MainPage(self.d)
# 入口
location = 'com.zhenai.loveguider:id/edittext'
self.clear_input_x_by_resourceId(location, workerId, 0)
self.clear_input_x_by_resourceId(location, password, 1)
sleep(1)
self.d.xpath("//*[@text='立即登录']").click()
sleep(2)
return MainPage(self.d)
if __name__ == '__main__':
LoverPadApp().clear()
Page包:
- 这里封装企业微信所有要测试的页面
- 思路:一个页面封装为一个类,页面上的每个功能封装为一个方法
- 另外元素的定位方法,大家视情况而定:
1)如果APP很多id是动态生成的话,就建议使用:Xpath定位,因为id可能不是唯一的,是动态生成的,换一台设备后,原代码可能就会因为找不到id而无法运行。
为了提高代码设计的兼容性,我们尽量考虑到以后前端元素变更又或者测试设备更换的情况
2)如果Id是唯一的,就建议用id查找,因为Xpath定位比id查找慢
"""
恋爱宝首页
"""
from mikasa_script.pad_ui_auto.base.LoverPadApp import LoverPadApp
from mikasa_script.pad_ui_auto.page.MemberLoginPage import MemberLoginPage
class MainPage(LoverPadApp):
# python 类型提示
def __init__(self, d):
# 拿到上个页面的driver
self.d = d
def click_member_login(self):
"""
点击会员登录按钮
"""
if self.d != None:
return MemberLoginPage(self.d)
# 如果有弹窗,点击始终允许
self.close_popup("始终允许", None, None)
location = "//*[@text='登录']"
self.d.xpath(location).click()
self.close_popup("始终允许", None, None)
return MemberLoginPage(self.d)
2、创建会员登录页面,MemberLoginPage.py
"""
恋爱宝会员登录页
"""
from time import sleep
from mikasa_script.pad_ui_auto.base.LoverPadApp import LoverPadApp
from mikasa_script.pad_ui_auto.page.MemberMainPage import MemberMainPage
class MemberLoginPage(LoverPadApp):
# python 类型提示
def __init__(self, d):
# 拿到上个页面的driver
self.d = d
def member_login(self, memberId):
"""
会员到店登录
"""
# if self.d is not None:
# return MemberMainPage(self.d)
self.d.xpath("//*[@text='请输入会员手机号或ID']").click()
self.d.send_keys(memberId)
self.d.xpath("//*[@text='请选择接待包间']").click()
sleep(1)
self.d.xpath("//*[@text='测试包间']").click()
sleep(2)
self.d(text="进入包间").click()
sleep(2)
return MemberMainPage(self.d)
3、创建会员页面,MemberMainPage.py
"""
恋爱宝:会员界面
"""
from time import sleep
from mikasa_script.pad_ui_auto.base.LoverPadApp import LoverPadApp
from mikasa_script.pad_ui_auto.page.CommitLetterPage import CommitLetterPage
from mikasa_script.pad_ui_auto.page.EmoCounselingPage import EmoCounselingPage
from mikasa_script.pad_ui_auto.page.EmoTestPage import EmoTestPage
from mikasa_script.pad_ui_auto.page.HighVipPage import HighVipPage
from mikasa_script.pad_ui_auto.page.IdCardPage import IdCardPage
from mikasa_script.pad_ui_auto.page.ImgEvaluationPage import ImgEvaluationPage
from mikasa_script.pad_ui_auto.page.MemberInfoPage import MemberInfoPage
from mikasa_script.pad_ui_auto.page.MyOrderPage import MyOrderPage
class MemberMainPage(LoverPadApp):
def __init__(self, d):
# 拿到上个页面的driver
self.d = d
def check_room_exist(self):
"""
校验包间名字是否存在
"""
element = self.d(text="包间:测试包间")
if element:
return True
else:
return False
def click_info_table_button(self):
"""
点击信息表按钮
"""
# if self.d is not None:
# return MemberInfoPage(self.d)
self.d.xpath("//*[@text='信息表']").click()
sleep(2)
return MemberInfoPage(self.d)
def click_emo_test_button(self):
"""
点击情感测评按钮
"""
self.d.xpath("//*[@text='情感测评']").click()
sleep(2)
return EmoTestPage(self.d)
def click_img_evaluation_button(self):
"""
点击形象评估按钮
"""
self.d.xpath("//*[@text='形象评估']").click()
sleep(2)
return ImgEvaluationPage(self.d)
def click_id_card_button(self):
"""
点击身份证件按钮
"""
self.d.xpath("//*[@text='身份证件']").click()
sleep(2)
return IdCardPage(self.d)
def click_commit_letter_button(self):
"""
点击承诺函按钮
"""
self.d.xpath("//*[@text='承诺函']").click()
sleep(2)
return CommitLetterPage(self.d)
def click_my_order_button(self):
"""
点击我的订单按钮
"""
self.d.xpath("//*[@text='我的订单']").click()
sleep(2)
return MyOrderPage(self.d)
def click_high_vip_button(self):
"""
点击高端VIP服务按钮
"""
self.d.xpath("//*[@resource-id='com.zhenai.loveguider:id/service_rv']/android.widget.FrameLayout[1]").click()
sleep(2)
return HighVipPage(self.d)
def click_emo_counseling_button(self):
"""
点击情感咨询服务按钮
"""
self.d.xpath("//*[@resource-id='com.zhenai.loveguider:id/service_rv']/android.widget.FrameLayout[2]").click()
sleep(2)
return EmoCounselingPage(self.d)
4、创建会员信息表页面,MemberInfoPage.py
"""
恋爱宝:会员信息表界面
"""
from time import sleep
from mikasa_script.pad_ui_auto.base.LoverPadApp import LoverPadApp
class MemberInfoPage(LoverPadApp):
def __init__(self, d):
# 拿到上个页面的driver
self.d = d
def check_info_view(self):
"""
校验信息填写后,是否是信息资料预览界面
"""
self.d.xpath("//*[@text='信息表']").click()
sleep(2)
element = self.d(text="择偶标准")
if element:
return False
else:
return True
def input_member_info(self):
"""
填写会员信息
"""
self.close_popup("我已知晓", None, None)
sleep(0.5)
if self.d != None:
# 页面下滑,移动到有修改信息按钮,如果有则点击修改
update_button = "//*[@resource-id='com.zhenai.loveguider:id/change']"
self.swipe_for_click(update_button, wait_after_click=0.0)
sleep(2)
self.close_popup("我已知晓", None, None)
next_step = "//*[@resource-id='com.zhenai.loveguider:id/btn_next']"
self.swipe_for_click(next_step, wait_after_click=0.0)
sleep(2)
self.d.xpath("//*[@resource-id='com.zhenai.loveguider:id/bottom_btn_next']").click() # 下一步
sleep(2)
# 填表人确认
self.d.xpath('//*[@resource-id="com.zhenai.loveguider:id/commitment_agree1"]').click() # 我承诺
self.d.xpath('//*[@resource-id="com.zhenai.loveguider:id/bottom_btn_next"]').click() # 提交并预览
self.close_popup("继续保存", None, None)
# self.d.xpath('//*[@resource-id="com.zhenai.loveguider:id/tv_left"]').click() # 不了,继续保存
MemberInfoPage.back(self, 1)
return MemberInfoPage(self.d)
# 基本信息填写
# self.clear_input_by_resourceId("//*[@resource-id='com.zhenai.loveguider:id/et_name']", "杨椰")
# self.d(resourceId="com.zhenai.loveguider:id/rb_sex_female").click()
# self.d(resourceId="com.zhenai.loveguider:id/select_info_tv").click()
# self.d.xpath(
# "//*[@resource-id='com.zhenai.loveguider:id/select_ll_marriage']/android.widget.RelativeLayout[1]"
# "/android.widget.TextView[2]").click() # 婚况
# self.d(text="确定").click()
# self.d.xpath("//*[@resource-id='com.zhenai.loveguider:id/select_ll_place']/android.widget.RelativeLayout[1]"
# "/android.widget.TextView[2]").click() # 籍贯
# self.d(text="确定").click()
# self.d.xpath("//*[@resource-id='com.zhenai.loveguider:id/select_ll_census']/android.widget.RelativeLayout[1]/"
# "android.widget.TextView[2]").click() # 户籍
# self.d(text="确定").click()
# self.d(resourceId="com.zhenai.loveguider:id/et_income").click()
# self.d.send_keys("10000") # 月收入
# # TODO 屏幕滑动
#
# self.d.xpath('//*[@text="请选择"]').click() # 职业
# self.d(text="确定").click()
# self.d.xpath('//*[@resource-id="com.zhenai.loveguider:id/rg_house_no"]').click() # 购房
# self.d.xpath('//*[@resource-id="com.zhenai.loveguider:id/no_buy_house_member_ll"]'
# '/android.widget.RelativeLayout[1]/android.widget.TextView[2]').click() # 未购房详情
# self.d(text="确定").click()
# self.d.xpath('//*[@resource-id="com.zhenai.loveguider:id/rg_car_no"]').click() # 购车
# self.d.xpath('//*[@resource-id="com.zhenai.loveguider:id/select_ll_marry_time"]'
# '/android.widget.RelativeLayout[1]/android.widget.TextView[2]').click() # 计划结婚时间
# self.d(text="确定").click()
# self.d.xpath(
# '//*[@resource-id="com.zhenai.loveguider:id/select_ll_comment_self"]/'
# 'android.widget.RelativeLayout[1]/android.widget.TextView[2]').click() # 自我评价
# self.d.xpath(
# '//*[@resource-id="com.zhenai.loveguider:id/singleChoiceLv"]/'
# 'android.widget.LinearLayout[1]/android.widget.FrameLayout[1]/android.widget.CheckBox[1]').click() # 勾选
# self.d(text="确定").click()
# self.d.xpath('//*[@resource-id="com.zhenai.loveguider:id/btn_next"]').click()
# sleep(2)
#
# # 择偶标准
# self.d.xpath(
# '//*[@resource-id="com.zhenai.loveguider:id/select_ll_height_range"]/android.widget.RelativeLayout[1]/android.widget.TextView[2]').click() # 身高范围
# self.d(text="确定").click()
#
# self.d.xpath(
# '//*[@resource-id="com.zhenai.loveguider:id/select_ll_marriage_requirement"]/android.widget.RelativeLayout[1]/android.widget.TextView[2]').click() # 婚况要求
# self.d(text="确定").click()
#
# self.d.xpath(
# '//*[@resource-id="com.zhenai.loveguider:id/select_ll_plage_range"]/'
# 'android.widget.RelativeLayout[1]/android.widget.TextView[2]').click() # 籍贯范围
# self.d(text="确定").click()
# self.d.xpath(
# '//*[@resource-id="com.zhenai.loveguider:id/select_ll_education_range"]/'
# 'android.widget.RelativeLayout[1]/android.widget.TextView[2]').click() # 学历范围
# self.d.xpath(
# '//*[@resource-id="com.zhenai.loveguider:id/multiChoiceLv"]/android.widget.LinearLayout[5]/'
# 'android.widget.FrameLayout[1]/android.widget.CheckBox[1]').click() # 勾选本科
#
# self.d(text="确定").click()
# self.d.xpath(
# '//*[@resource-id="com.zhenai.loveguider:id/select_ll_income_range"]/'
# 'android.widget.RelativeLayout[1]/android.widget.TextView[2]').click() # 月收入范围
# self.d(text="确定").click()
# self.d.xpath(
# '//*[@resource-id="com.zhenai.loveguider:id/select_ll_care"]/'
# 'android.widget.RelativeLayout[1]/android.widget.TextView[2]').click() # 最在意项
# self.d.xpath(
# '//*[@resource-id="com.zhenai.loveguider:id/multiChoiceLv"]/'
# 'android.widget.LinearLayout[1]/android.widget.FrameLayout[1]/android.widget.CheckBox[1]').click() # 最在意项
# self.d(text="确定").click()
# self.d.xpath('//*[@resource-id="com.zhenai.loveguider:id/et_jihui"]').click() # 勾选
# self.d.send_keys("暂无忌讳项")
# self.d.xpath('//*[@resource-id="com.zhenai.loveguider:id/bottom_btn_next"]"]').click() # 下一步
#
# # 填表人确认
# self.d.xpath('//*[@resource-id="com.zhenai.loveguider:id/rb_self_yes"]').click() # 勾选是本人
# self.d.xpath('//*[@resource-id="com.zhenai.loveguider:id/commitment_agree1"]').click() # 我承诺
# self.d.xpath('//*[@resource-id="com.zhenai.loveguider:id/bottom_btn_next"]').click() # 提交并预览
# self.d.xpath('//*[@resource-id="com.zhenai.loveguider:id/tv_left"]').click() # 不了,继续保存
# 点击返回,返回上一页
# self.d.xpath('//*[@resource-id="com.zhenai.loveguider:id/iv_back"]').click()
case包:
- 封装测试用例类
from mikasa_script.pad_ui_auto.base.LoverPadApp import LoverPadApp
from mikasa_script.pad_ui_auto.page.MemberMainPage import MemberMainPage
from mikasa_script.pad_ui_auto.utils.crm_api import *
workerId = "2046737"
pad_pwd = "yy1998123"
memberId = "1443736269"
crm_pwd = "AC2211AA1C44AFD3D08F3F09DFBF77CB"
phone = "199****7590"
verifyCode = "222222"
class TestLoverPad:
def setup_class(self):
# 用例开始之前,先确认用户进行到店
sid = get_crm_sid(crm_pwd, phone, workerId, verifyCode)
if not check_member_enter_shop(memberId, workerId, sid):
# 若会员不存在到店记录,则进行到店操作
sure_to_shop(workerId, memberId, sid)
def setup(self):
# 每个用例开始之前,启动恋爱宝
self.app = LoverPadApp()
pass
def teardown(self):
# 每个用例执行完成后,就默认返回到首页
# self.app.clear()
pass
# self.app.back()
def teardown_class(self):
# 所有用例都结束后,则关闭企业微信
# self.app.stop()
pass
# 用例一、会员登录
def test_member_login(self):
result = LoverPadApp().start().login_goto_main(workerId, pad_pwd).click_member_login().member_login(
memberId).check_room_exist()
# 断言包间名称是否存在
assert True == result
# 用例二、测试信息表界面
def test_info_table(self):
result = MemberMainPage(LoverPadApp().start()).click_info_table_button().input_member_info().check_info_view()
assert True == result
# 用例三、测试情感测评界面
def test_emo_test(self):
MemberMainPage(LoverPadApp().start()).click_emo_test_button().write_emo_test()



