python + uiautomator2 常用公共方法封装
前言
由于公司UI自动化框架底层用的是Uiautomator2,所以我就用Uiautomator2搭了一套UI自动化框架,思路其实和Appnium一样的。
uiautomator2是一个自动化测试开源工具,仅支持android平台的自动化测试,其封装了谷歌自带的uiautomator2测试框架;
u2 现在google 官方使用的是apk的形式来实现的,有大神封装了python来实现u2的功能的使用。
具体的了解相关的功能和实现的原理可以查看开源库:github 的地址:https://github.com/openatx/uiautomator2
ui2 的下载安装与环境配置等,见之前写的一篇帖子:https://www.cnblogs.com/gancuimian/p/16725664.html
ui2 的常用方法使用(未封装),见之前写的一篇帖子:https://www.cnblogs.com/gancuimian/p/16947337.html
整体框架介绍:(非固定模式,每个人的习惯不同,框架会有些出入,有些包可以是非必要)
框架搭建
ps:这里主要讲 common 包下面的公共方法类(basepage.py模块)的封装,其它包/模块不做详细介绍
先创建一个BasePage.py
-
为什么要单独封装一个BasePage呢? 如果说以后我们不用uiautomator2这个框架了,我们只需要更改BasePage即可,不会影响到其他类的代码。
- 另外,这个类也可以封装自己写的公用的方法,例如:重复性很高的代码,这些方法不论在哪个app里都能用的话,我们就单独拧出来封装成一个方法。
模块创建完成后,先导入需要用到的内置库或需要提前安装的第三方库。
1 import os 2 import re 3 import time 4 import random 5 from typing import Union 6 from data.Swipe_Direction import SwipeDirection
# 第6行导入的是下方的一个类;在下面代码 207 行的方法中有引用。
下面代码为本人工作中会用到的一些操作 方法的封装。
1 import os 2 import re 3 import time 4 import random 5 from typing import Union 6 from data.Swipe_Direction import SwipeDirection 7 8 9 class BasePage: # 构造函数 10 def __init__(self, driver): 11 self.driver = driver 12 13 # 点击 14 def click(self, element, sleepTime=0): 15 if str(element).startswith("com"): # 若开头是com则使用ID定位 16 self.driver(resourceId=element).click() # 点击定位元素 17 elif re.findall("//", str(element)): # 若//开头则使用正则表达式匹配后用xpath定位 18 self.driver.xpath(element).click() # 点击定位元素 19 else: # 若以上两种情况都不是,则使用描述定位 20 self.driver(description=element).click() # 点击定位元素 21 time.sleep(sleepTime) 22 23 24 # 点击直到元素消失 25 def click_until_gone(self, element, kind): 26 if kind == "id": 27 self.driver(resourceId=element).click_gone() 28 elif kind == "class": 29 self.driver(className=element).click_gone() 30 elif kind == "text": 31 self.driver(text=element).click_gone() 32 else: # 若以上两种情况都不是,则使用描述定位 33 self.driver(description=element).click_gone() # 点击定位元素 34 35 # 组合定位classname和text 36 def click_by_classname_text(self, element1, element2): 37 self.driver(className=element1, text=element2).click() 38 39 # 组合定位classname和description 40 def click_by_classname_description(self, element1, element2): 41 self.driver(className=element1, description=element2).click() 42 43 # 组合定位text和description 44 def click_by_text_description(self, element1, element2): 45 self.driver(text=element1, description=element2).click() 46 47 48 # 根据id点击(包括非com开头的id点击定位元素) 49 def click_by_id(self, element, sleepTime=0): 50 self.driver(resourceId=element).click() 51 time.sleep(sleepTime) 52 53 54 # 根据文本点击 55 def click_by_text(self, element, sleepTime=0): 56 self.driver(text=element).click() # 点击定位元素 57 time.sleep(sleepTime) 58 59 60 # 根据百分比坐标点击 61 def click_by_zuobiao(self, x, y, sleepTime=0): 62 size = self.driver.window_size() 63 self.driver.click(int(size[0] * x), int(size[1] * y)) 64 time.sleep(sleepTime) 65 66 67 # 根据坐标点击元素 68 def click_coord(self, x, y): 69 self.driver.click(x, y) 70 71 72 # 根据坐标双击元素 73 def double_click_by_zuobiao(self, x, y, sleepTime=0): 74 size = self.driver.window_size() 75 self.driver.double_click(int(size[0] * x), int(size[1] * y)) 76 time.sleep(sleepTime) 77 78 79 # 超时时间设置点击,根据文本定位,针对点击屏幕元素反应慢的元素 80 def click_by_text_time_out(self, element, timeout=30, sleepTime=0): 81 self.driver(text=element).click(timeout=timeout) 82 time.sleep(sleepTime) 83 84 85 # 长按 86 def long_click_extend(self, element, dur=1, sleepTime=0): 87 zhmodel = re.compile(u'[\u4e00-\u9fa5]') 88 if str(element).startswith("com"): # 若开头是com则使用ID定位 89 self.driver(resourceId=element).long_click(dur) # 点击定位元素 90 elif re.findall("//", str(element)): # 若//开头则使用正则表达式匹配后用xpath定位 91 self.driver.xpath(element).long_click() # 点击定位元素 92 elif zhmodel.search(str(element)): 93 self.driver(description=element).long_click(dur) 94 else: 95 self.driver(className=element).long_click(dur) 96 time.sleep(sleepTime) 97 98 99 # 通过文本长击 100 def long_click_by_text(self, element, dur=0.5, sleepTime=0): 101 self.driver(text=element).long_click(dur) 102 time.sleep(sleepTime) 103 104 105 # 通过坐标长击 106 def long_click_by_zuobiao(self, x, y, sleepTime=0,duration: float = 1): 107 self.driver.long_click(x, y,duration) 108 time.sleep(sleepTime) 109 110 111 # 清空输入框中的内容 112 def clear(self, element): 113 if str(element).startswith("com"): # 若开头是com则使用ID定位 114 self.driver(resourceId=element).clear_text() # 清除文本 115 elif re.findall("//", str(element)): # 若//开头则使用正则表达式匹配后用xpath定位 116 self.driver.xpath(element).clear_text() # 清除文本 117 else: # 若以上两种情况都不是,则使用描述定位 118 self.driver(description=element).clear_text() # 清除文本 119 120 121 # 输入 122 def input(self, element, value, sleepTime=0): 123 if str(element).startswith("com"): # 若开头是com则使用ID定位 124 self.driver(resourceId=element).set_text(value) # send_keys 125 elif re.findall("//", str(element)): # 若//开头则使用正则表达式匹配后用xpath定位 126 self.driver.xpath(element).set_text(value) 127 else: # 若以上两种情况都不是,则使用描述定位 128 self.driver(description=element).set_text(value) 129 time.sleep(sleepTime) 130 131 # 不存在搜索按钮的搜索 132 def input_by_send_keys(self, element, value): 133 self.driver.set_fastinput_ime(True) # 切换成FastInputIME输入法 134 if str(element).startswith("com"): # 若开头是com则使用ID定位 135 self.driver(resourceId=element).send_keys(value) # send_keys 136 elif re.findall("//", str(element)): # 若//开头则使用正则表达式匹配后用xpath定位 137 self.driver.xpath(element).send_text(value) 138 else: # 若以上两种情况都不是,则使用描述定位 139 self.driver(description=element).send_keys(value) 140 self.driver.set_fastinput_ime(False) # 切换成正常的输入法 141 self.driver.send_action("search") # 模拟输入法的搜索 142 143 144 # 查找元素 145 def find_elements(self, element, timeout=5): # 找元素 146 is_exited = False 147 try: 148 while timeout > 0: 149 xml = self.driver.dump_hierarchy() # 获取网页层次结构 150 if re.findall(element, xml): 151 is_exited = True 152 break 153 else: 154 timeout -= 1 155 except: 156 print("元素未找到!") 157 finally: 158 return is_exited 159 160 161 # 断言元素是否存在, 不能判定xpath元素 162 def assert_existed(self, element): # 163 # assert self.find_elements(element) == True, "断言失败,{}元素不存在!".format(element) 164 return self.find_elements(element) == True 165 166 # 判断元素是否存在,如随机弹窗等 167 def judge_existed(self, element, type: str = "text", timeout=2): 168 if re.findall("//", str(element)): # 若//开头则使用正则表达式匹配后用xpath定位 169 return self.driver.xpath(element).exists == True 170 elif type == "text": 171 return self.driver(text=element).exists(timeout=timeout) == True 172 elif type == "dec": 173 return self.driver(description=element).exists(timeout=timeout) == True 174 else: 175 pass 176 177 178 # 截图 179 def screenshot(self, imageName): 180 if os.path.exists(r"./images"): 181 if os.path.exists(fr"./images/{imageName}.png"): 182 image = self.driver.screenshot() 183 image.save(fr"./images/{imageName}_bak.png") 184 else: 185 image = self.driver.screenshot() 186 image.save(fr"./images/{imageName}.png") 187 else: 188 os.mkdir(r"./images") 189 image = self.driver.screenshot() 190 image.save(fr"./images/{imageName}.png") 191 192 193 # 拿取文本 194 def get_text_extend(self, element=None, type: str = "id"): 195 196 if re.findall("//", str(element)): # 若//开头则使用正则表达式匹配后用xpath定位 197 return self.driver.xpath(element).get_text() 198 elif type == "id": 199 return self.driver(resourceId=element).get_text() 200 elif type == "selected": 201 return self.driver(selected=True).get_text() 202 else: 203 pass 204 205 206 # 滑动 (正常屏幕滑动,向上滑动解锁,返回主界面,解锁等通用) 207 # 坐标支持数据类型:Union[int, str] 208 def swipe_extend(self, x1=0.5, y1=0.99, x2=0.5, y2=0.3, dur: Union[int, str] = 0.2, 209 sleepTime=0, type: str = "percent"): 210 if type == "percent": 211 size = self.driver.window_size() 212 x1 = int(size[0] * x1) 213 y1 = int(size[1] * y1) 214 x2 = int(size[0] * x2) 215 y2 = int(size[1] * y2) 216 self.driver.swipe(x1, y1, x2, y2, dur) 217 time.sleep(sleepTime) 218 else: 219 self.driver.swipe(x1, y1, x2, y2, dur) 220 time.sleep(sleepTime) 221 222 223 # 按下之后滑动,长按滑动 224 def long_click_swipe(self, x1, y1, x2, y2, dur=0.8, sleepTime=0): 225 self.driver.touch.down(x1, y1).sleep(dur).move(x1, y1).move(x2, y2).up(x2, y2) 226 time.sleep(sleepTime) 227 228 229 # 向上滑动解锁,返回主界面,解锁用 230 def swipe_up(self, time=0.2): 231 size = self.driver.window_size() 232 x1 = int(size[0] * 0.5) 233 y1 = int(size[1] * 1) 234 y2 = int(size[1] * 0.3) 235 self.driver.swipe(x1, y1, x1, y2, time) 236 237 238 # 滑动,根据方向滑动 239 def swipe_ext_extend(self, direction: Union[SwipeDirection, str] = "up", scale=0.9, sleepTime=0): 240 self.driver.swipe_ext(direction, scale=scale) 241 time.sleep(sleepTime) 242 243 244 # 缩放 245 def pinch_extend(self, element, kind: str = "out or in", percent=100, steps=50): 246 """ 在元素上面,做两个手指缩放的操作,kind in 或者out,放大或者缩小""" 247 zhmodel = re.compile(u'[\u4e00-\u9fa5]') 248 if str(element).startswith("com"): 249 selector = self.driver(resourceId=element) 250 elif not zhmodel.search(str(element)): 251 selector = self.driver(className=element) 252 elif zhmodel.search(str(element)): # 若以上两种情况都不是,则使用描述定位 253 selector = self.driver(description=element) 254 255 if kind == "in": 256 selector.pinch_in(percent, steps) 257 elif kind == "out": 258 selector.pinch_out(percent, steps) 259 else: 260 raise Exception("输入kind不能是非in/out") 261 262 263 # 关机 264 def reboot_physical_key(self): 265 self.driver.shell("sendevent /dev/input/event0 1 116 1") 266 self.driver.shell("sendevent /dev/input/event0 0 0 0") 267 time.sleep(3) 268 self.driver.shell("sendevent /dev/input/event0 1 116 0") 269 self.driver.shell("sendevent /dev/input/event0 0 0 0") 270 time.sleep(1) 271 self.click_by_text("关闭电源") 272 273 274 # 截图 275 def screenshot_physical_key(self): 276 self.driver.shell("sendevent /dev/input/event0 1 114 1") 277 self.driver.shell("sendevent /dev/input/event0 0 0 0") 278 self.driver.shell("sendevent /dev/input/event0 1 116 1") 279 self.driver.shell("sendevent /dev/input/event0 0 0 0") 280 self.driver.shell("sendevent /dev/input/event0 1 116 0") 281 self.driver.shell("sendevent /dev/input/event0 0 0 0") 282 self.driver.shell("sendevent /dev/input/event0 1 114 0") 283 self.driver.shell("sendevent /dev/input/event0 0 0 0") 284 285 286 # 推送文件到手机 287 def push_extend(self, root: Union[list, str], target, sleepTime=1): 288 peojectPath = "\\".join(os.path.abspath(os.path.dirname(__file__)).split("\\")[:-1]) 289 if isinstance(root, list): 290 for i in root: 291 self.driver.push(peojectPath+i, target) 292 elif isinstance(root, str): 293 self.driver.push(root, target) 294 time.sleep(sleepTime) 295 296 297 def randmon_phone(self): 298 """ 随机生成一个手机号,或者其他想生成的数据 """ 299 while True: 300 phone = "AAAAA新建" 301 for i in range(8): 302 num = random.randint(0, 9) 303 phone += str(num) 304 return phone 305 306 307 def power(self, kind: str='power or kill'): 308 '''模拟power键''' 309 if kind == 'power': 310 self.driver.screen_on() # 息屏 311 elif kind == 'kill': 312 self.driver.screen_off() # 亮屏 313 else: 314 raise Exception("输入kind有误") 315 316 317 def virtual_key(self,kind: str = "home or delete or up or down or volume_up or volume_down or volume_mute or power or back"): 318 """ 常用虚拟按键 """ 319 if kind == "home": 320 self.driver.press("home") # 点击home键 321 elif kind == "delete": 322 self.driver.press("delete") # 点击删除键 323 elif kind == "up": 324 self.driver.press("up") # 点击上键 325 elif kind == "down": 326 self.driver.press("down") # 点击下键 327 elif kind == "volume_up": 328 self.driver.press("volume_up") # 点击音量+ 329 elif kind == "volume_down": 330 self.driver.press("volume_down") # 点击音量- 331 elif kind == "volume_mute": 332 self.driver.press("volume_mute") # 点击静音 333 elif kind == "power": 334 self.driver.press("power") # 点击电源键 335 elif kind == "back": 336 self.driver.press("back") # 点击返回键 337 else: 338 raise Exception("输入kind有误")
以上为个人常用公共方法封装,但不是全部,有些场景可能未覆盖到。更多的 ui2 相关知识可自行网上学习。
随机推荐几个ui2相关的帖子,更多的ui2的相关知识自行网上搜索了解。
https://blog.csdn.net/Makasa/article/details/124358921
https://ceshiren.com/t/topic/5396
https://blog.csdn.net/weixin_43444734/article/details/124703281
更新几个uiautomaor2公共方法的封装
def screen_off(self): '''虚拟按键 # 息屏''' self.driver.screen_off() def screen_on(self): """ 虚拟按键 # 亮屏""" self.driver.screen_on() def press_key(self, key,sleepTime=2): """虚拟按键的公共方法""" self.driver.press(key) time.sleep(sleepTime) # 使用示例 # 假设你有一个driver对象,可以是任何支持press方法的对象 # driver = YourDriverObject() # press_key(driver, "recent") # 模拟最近键 # press_key(driver, "home") # 模拟Home键 # press_key(driver, "menu") # 模拟菜单键 def press_key_code(self,key_code,sleepTime=2): """ 模拟虚拟按键,与上面的几个方法大同小异""" try: # 使用adb shell input keyevent命令模拟按键事件 subprocess.check_call(['adb', 'shell', 'input', 'keyevent', str(key_code)]) print(f"按键 {key_code} 被模拟点击") except subprocess.CalledProcessError as e: print("执行失败:", e) except Exception as e: print("发生错误:", e) time.sleep(sleepTime) # # 示例:模拟点击Home键,Home键的键值通常是3 # press_key(3) # # 示例:模拟点击菜单键,菜单键的键值通常是82 # press_key(82) # 更多按键对应数字请参考:https://www.cnblogs.com/gancuimian/p/17405701.html def run_adb_command(self,command,sleepTime=0): """ 执行 ADB 命令的封装方法 :param command: 占位符,调用方法时输入adb命令 :param sleepTime: 休眠时间 """ try: # 使用shell=True来直接执行字符串形式的命令 subprocess.check_call(command, shell=True) log.info("adb命令执行成功") except subprocess.CalledProcessError as e: log.error(f"adb命令执行失败{e}") time.sleep(sleepTime) # 示例: # enable_driving_mode("adb shell xxxxx") def get_toast_text(self, timeout=2): """ :param timeout:等待Toast弹窗显示的最大时间(秒) :return: Toast弹窗的文本,如果没有找到则返回default """ try: start_time = time.time() while time.time() - start_time < timeout: # 尝试获取Toast弹窗的文本 toast_msg = self.driver.toast.get_message() # 短暂等待后再次检查 time.sleep(0.3) log.info(f"获取到toast弹窗内容:{toast_msg}") return toast_msg except Exception as e: print(f"获取Toast弹窗时发生错误:{e}") return None # 使用示例 # toast_values = self.get_toast_text() # 截图的封装, def get_file_path(self,dir_name, filename): """Construct the full path for the screenshot file with .png extension.""" # Ensure the file name has a .png extension if not filename.lower().endswith('.png'): filename += '.png' return os.path.join(dir_name, filename) # 截图的封装,保存在screenshots目录下,并在allure报告中展示截图 def take_and_attach_screenshot(self, step_title, file_name, dir_name='screenshots'): """Take a screenshot and attach it to the Allure report.""" # Ensure the screenshots directory exists if not os.path.exists(dir_name): os.makedirs(dir_name) file_path = self.get_file_path(dir_name, file_name) self.driver.screenshot(file_path) # 使用uiautomator2的screenshot方法 with allure.step(step_title): with open(file_path, 'rb') as file: # 以二进制模式打开文件,读取内容 file_content = file.read() allure.attach(file_content, name=step_title, attachment_type=allure.attachment_type.PNG)