前言
由于公司UI自动化框架底层用的是Uiautomator2,所以我就用Uiautomator2搭了一套UI自动化框架,并运用某软件做了一个实战,思路其实和之前写的Appnium一样的
一、Uiautomator2介绍
- 这篇文章写的很详细了大家可以学习学习,我就不再介绍了
链接:https://ceshiren.com/t/topic/5396
二、整体框架介绍
base包:封装基础类
case包:封装测试用例类
datas包:参数化数据(excel 或者 yml...)
logs包:封装日志
page包:封装所有的页面
sms包:发送邮箱
report包:存放测试报告
utils包:工具包,例如读取参数化数据
三、框架搭建
1、Base包:
1.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)
1.2、创建一个LoverPadApp.py,继承BasePage
- 由于以后可能又去测试其他的App,所以我们这边就单独再创建一个LoverPadApp的类继承BasePage,为了区分App
"""
具体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('具体APP')
return self.d
def restart(self):
"""
重启App
"""
pass
def clear(self):
"""
清除app缓存
"""
self.d.app_clear('具体APP')
def stop(self):
"""
关闭App
"""
self.d.app_stop('具体APP')
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()
2、Page包:
- 这里封装企业微信所有要测试的页面
- 思路:一个页面封装为一个类,页面上的每个功能封装为一个方法
- 另外元素的定位方法,大家视情况而定:
1)如果APP很多id是动态生成的话,就建议使用:Xpath定位,因为id可能不是唯一的,是动态生成的,换一台设备后,原代码可能就会因为找不到id而无法运行。
为了提高代码设计的兼容性,我们尽量考虑到以后前端元素变更又或者测试设备更换的情况
2)如果Id是唯一的,就建议用id查找,因为Xpath定位比id查找慢
2.1、创建首页,MainPage.py
"""
首页
"""
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.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)
2.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)
2.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)
3、logs包:
3.1、创建log_utils.py
"""
该类为日志封装类
"""
import logging
import os
# 实例化logger对象
from mikasa_script.mikasa_auto.workwx_api_autotest.utils.utils import Utils
logger = logging.getLogger(__name__)
# 定义日志文件路径
log_path = Utils.get_log_path()
# 判断路径是否存在,不存在就创建
if not os.path.exists(log_path):
os.mkdir(log_path)
# 绑定log的handler
file_handler = logging.FileHandler(filename=f"{log_path}/api_object.log", encoding="utf-8")
# 输出的formatter
formatter = logging.Formatter(
'[%(asctime).19s] %(process)d%(levelname).1s %(filename)s:%(lineno)d:%(funcName)s: %(message)s]')
# 日志格式与句柄的绑定
file_handler.setFormatter(formatter)
# 控制台句柄定义
steam_handler = logging.StreamHandler()
# 日志格式与句柄的绑定
steam_handler.setFormatter(formatter)
# 与logging进行绑定
logger.addHandler(file_handler)
logger.addHandler(steam_handler)
# 设置展示/写入文件的日志的级别
logger.setLevel(logging.INFO)
4、utils包:
4.1、创建utils.py
"""
该类为工具类
包括读取yaml文件/读取log日志文件路径
"""
import os
import jsonpath
import yaml
class Utils:
@classmethod
def get_yaml_data(self, yaml_file):
"""
封装提取yaml文件的方法
:param yaml_file: 传入文件的路径
:return:
"""
with open(f"{Utils.get_data_path()}/{yaml_file}", encoding="utf-8") as f:
datas = yaml.safe_load(f)
return datas
@classmethod
def get_data_path(self):
"""
获取data文件路径
:return:
"""
path = os.sep.join([os.path.dirname(os.path.abspath(__file__)), "../data"])
print("path", path)
return path
@classmethod
def get_log_path(cls):
"""
获取日志文件路径
:return:
"""
path = os.sep.join([os.path.dirname(os.path.abspath(__file__)), "../logs"])
print("path", path)
return path
@classmethod
def jsonpath_utils(cls, obj, expt):
"""
json处理工具
:param obj:
:param expt:
:return:
"""
return jsonpath.jsonpath(obj, expt)
if __name__ == '__main__':
path = Utils.get_data_path()
print(Utils.get_yaml_data("/corp_data.yaml"))
5、data包:
5.1、创建data.yaml
- 维护你的配置之类的东西 ,然后用utils里面的get_yaml_data方法读取对应的数据
workInfo:
workerId: 123456
pad_pwd: 123456
memberInfo:
memberId: 123456
crm_pwd: 123456
phone: 123456
verifyCode: 123456
6、sms包:
6.1、新建email.py
import io
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import os
# from HttpRunnerManager.settings import
subject = "自动化测试报告"
with io.open(html_report_path, 'r', encoding='utf-8') as stream:
send_file = stream.read()
att = MIMEText(send_file, "base64", "utf-8")
att["Content-Type"] = "application/octet-stream"
att["Content-Disposition"] = "attachment;filename = TestReports.html"
body = MIMEText("附件为定时任务生成的测试报告,请查收,谢谢!", _subtype='html', _charset='gb2312')
msg = MIMEMultipart('related')
msg['Subject'] = subject
msg['from'] = EMAIL_SEND_USERNAME
msg['to'] = receiver
msg.attach(att)
msg.attach(body)
smtp = smtplib.SMTP()
smtp.connect(smtp_server)
smtp.starttls()
smtp.login(EMAIL_SEND_USERNAME, EMAIL_SEND_PASSWORD)
smtp.sendmail(EMAIL_SEND_USERNAME, receiver.split(','), msg.as_string())
smtp.quit()
if __name__ == 'nt\\zhenai-testcases\\ui\\ZhenaiPad\\android\\登录注册\\results\\report.html')
7、case包:
- 编写测试用例类,关于allure生产测试报告自行下载,并进行环境变量配置
- allure下载地址:https://github.com/allure-framework/allure2/releases
- allure环境配置:https://www.jianshu.com/p/5134eee6594b
# 1、验证allure环境是否配置成功
allure --version
# 生成测试报告,执行allure命令:
pytest test_lover_pad.py --alluredir=./result --clean-alluredir #生成result文件
allure serve ./result #生成报告
7.1、test_lover_pad.py
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 = "xx"
pad_pwd = "xx"
memberId = "xx"
crm_pwd = "xx"
phone = "xx"
verifyCode = "xx"
@allure.feature('test_app')
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
# 用例一、会员登录
@allure.story('test_member_login')
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
# 用例二、测试信息表界面
@allure.story('test_info_table')
def test_info_table(self):
result = MemberMainPage(LoverPadApp().start()).click_info_table_button().input_member_info().check_info_view()
assert True == result
# 用例三、测试情感测评界面
@allure.story('test_emo_test')
def test_emo_test(self):
MemberMainPage(LoverPadApp().start()).click_emo_test_button().write_emo_test()