前言
由于公司UI自动化框架底层用的是Uiautomator2,所以我就用Uiautomator2搭了一套UI自动化框架,思路其实和Appnium一样的。
uiautomator2是一个自动化测试开源工具,仅支持android平台的自动化测试,其封装了谷歌自带的uiautomator2测试框架;
u2 现在google 官方使用的是apk的形式来实现的,有大神封装了python来实现u2的功能的使用。
整体框架介绍:(非固定模式,每个人的习惯不同,框架会有些出入,有些包可以是非必要)
框架搭建
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 相关知识可自行网上学习。
更新几个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)
# 更多按键对应数字请参考:
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)