Airtest是一個跨平臺的、基於圖像識別的UI自動化測試框架,適用於游戲和App,支持平臺有Windows、Android和iOS。Airtest框架基於一種圖形腳本語言Sikuli,引用該框架後,不再需要一行行的寫代碼,通過截取按鈕或輸入框的圖片,用圖片組成測試場景,這種方式學習成本低,簡單易上... ...
1 Airtest簡介
Airtest是一個跨平臺的、基於圖像識別的UI自動化測試框架,適用於游戲和App,支持平臺有Windows、Android和iOS。Airtest框架基於一種圖形腳本語言Sikuli,引用該框架後,不再需要一行行的寫代碼,通過截取按鈕或輸入框的圖片,用圖片組成測試場景,這種方式學習成本低,簡單易上手。
2 Airtest實踐
APP接入流水線過程中,賽博平臺只支持air腳本,因此需要對京管家APP的UI自動化腳本進行的改造。如截圖可見,AirtestIDE的主界面由菜單欄、快捷工具欄和多個視窗組成,初始佈局中的“設備視窗”是工具的設備連接交互區域。
air腳本生成步驟:
- 通過adb連接手機或模擬器
- 安裝應用APK
- 運行應用並截圖
- 模擬用戶輸入(點擊、滑動、按鍵)
- 卸載應用
通過以上步驟自動生成了 .air腳本,調試過程中我們可以在IDE中運行代碼,支持多行運行以及單行運行,調試通過後可在本地或伺服器以命令行的方式運行腳本:
.air腳本運行方式:airtest run “path to your .air dir” —device Android
.air腳本生成報告的方式:airtest report “path to your .air dir”
3 Airtest定位方式解析
IDE的log查看視窗會時時列印腳本執行的日誌,從中可以看出通過圖片解析執行位置的過程。下麵就以touch方法為例,解析Airtest如何通過圖片獲取到元素位置從而觸發點擊操作。
@logwrap
def touch(v, times=1, **kwargs):
"""
Perform the touch action on the device screen
:param v: target to touch, either a ``Template`` instance or absolute coordinates (x, y)
:param times: how many touches to be performed
:param kwargs: platform specific `kwargs`, please refer to corresponding docs
:return: finial position to be clicked, e.g. (100, 100)
:platforms: Android, Windows, iOS
"""
if isinstance(v, Template):
pos = loop_find(v, timeout=ST.FIND_TIMEOUT)
else:
try_log_screen()
pos = v
for _ in range(times):
G.DEVICE.touch(pos, **kwargs)
time.sleep(0.05)
delay_after_operation()
return pos
click = touch # click is alias of t
該方法通過loop_find獲取坐標,然後執行點擊操作 G.DEVICE.touch(pos, kwargs),接下來看loop_find如何根據模板轉換為坐標。
@logwrap
def loop_find(query, timeout=ST.FIND_TIMEOUT, threshold=None, interval=0.5, intervalfunc=None):
"""
Search for image template in the screen until timeout
Args:
query: image template to be found in screenshot
timeout: time interval how long to look for the image template
threshold: default is None
interval: sleep interval before next attempt to find the image template
intervalfunc: function that is executed after unsuccessful attempt to find the image template
Raises:
TargetNotFoundError: when image template is not found in screenshot
Returns:
TargetNotFoundError if image template not found, otherwise returns the position where the image template has
been found in screenshot
"""
G.LOGGING.info("Try finding: %s", query)
start_time = time.time()
while True:
screen = G.DEVICE.snapshot(filename=None, quality=ST.SNAPSHOT_QUALITY)
if screen is None:
G.LOGGING.warning("Screen is None, may be locked")
else:
if threshold:
query.threshold = threshold
match_pos = query.match_in(screen)
if match_pos:
try_log_screen(screen)
return match_pos
if intervalfunc is not None:
intervalfunc()
# 超時則raise,未超時則進行下次迴圈:
if (time.time() - start_time) > timeout:
try_log_screen(screen)
raise TargetNotFoundError('Picture %s not found in screen' % query)
else:
t
首先截取手機屏幕match_pos = query.match_in(screen),然後對比傳參圖片與截屏來獲取圖片所在位置match_pos = query.match_in(screen)。接下來看match_in方法的邏輯:
def match_in(self, screen):
match_result = self._cv_match(screen)
G.LOGGING.debug("match result: %s", match_result)
if not match_result:
return None
focus_pos = TargetPos().getXY(match_result, self.target_pos)
return focus_pos
裡面有個關鍵方法:match_result = self._cv_match(screen)
@logwrap
def _cv_match(self, screen):
# in case image file not exist in current directory:
ori_image = self._imread()
image = self._resize_image(ori_image, screen, ST.RESIZE_METHOD)
ret = None
for method in ST.CVSTRATEGY:
# get function definition and execute:
func = MATCHING_METHODS.get(method, None)
if func is None:
raise InvalidMatchingMethodError("Undefined method in CVSTRATEGY: '%s', try 'kaze'/'brisk'/'akaze'/'orb'/'surf'/'sift'/'brief' instead." % method)
else:
if method in ["mstpl", "gmstpl"]:
ret = self._try_match(func, ori_image, screen, threshold=self.threshold, rgb=self.rgb, record_pos=self.record_pos,
resolution=self.resolution, scale_max=self.scale_max, scale_step=self.scale_step)
else:
ret = self._try_match(func, image, screen, threshold=self.threshold, rgb=self.rgb)
if ret:
break
return ret
首先讀取圖片調整圖片尺寸,從而提升匹配成功率:
image = self._resize_image(ori_image, screen, ST.RESIZE_METHOD)
接下來是迴圈遍歷匹配方法for method in ST.CVSTRATEGY。而ST.CVSTRATEGY的枚舉值:
CVSTRATEGY = ["mstpl", "tpl", "surf", "brisk"]
if LooseVersion(cv2.__version__) > LooseVersion('3.4.2'):
CVSTRATEGY = ["mstpl", "tpl", "sift", "brisk"]
func = MATCHING_METHODS.get(method, None),func可能的取值有mstpl、tpl、surf、shift、brisk,無論哪種模式都調到了共同的方法_try_math
if method in ["mstpl", "gmstpl"]:
ret = self._try_match(func, ori_image, screen, threshold=self.threshold, rgb=self.rgb, record_pos=self.record_pos,
resolution=self.resolution, scale_max=self.scale_max, scale_step=self.scale_step)
else:
ret = self._try_match(func, image, screen, threshold=self.threshold, rgb=self.rgb)
而_try_math方法中都是調用的func的方法find_best_result()
@staticmethod
def _try_match(func, *args, **kwargs):
G.LOGGING.debug("try match with %s" % func.__name__)
try:
ret = func(*args, **kwargs).find_best_result()
except aircv.NoModuleError as err:
G.LOGGING.warning("'surf'/'sift'/'brief' is in opencv-contrib module. You can use 'tpl'/'kaze'/'brisk'/'akaze'/'orb' in CVSTRATEGY, or reinstall opencv with the contrib module.")
return None
except aircv.BaseError as err:
G.LOGGING.debug(repr(err))
return None
else:
return ret
以TemplateMatching類的find_best_result()為例,看一下內部邏輯如何實現。
@print_run_time
def find_best_result(self):
"""基於kaze進行圖像識別,只篩選出最優區域."""
"""函數功能:找到最優結果."""
# 第一步:校驗圖像輸入
check_source_larger_than_search(self.im_source, self.im_search)
# 第二步:計算模板匹配的結果矩陣res
res = self._get_template_result_matrix()
# 第三步:依次獲取匹配結果
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
h, w = self.im_search.shape[:2]
# 求取可信度:
confidence = self._get_confidence_from_matrix(max_loc, max_val, w, h)
# 求取識別位置: 目標中心 + 目標區域:
middle_point, rectangle = self._get_target_rectangle(max_loc, w, h)
best_match = generate_result(middle_point, rectangle, confidence)
LOGGING.debug("[%s] threshold=%s, result=%s" % (self.METHOD_NAME, self.threshold, best_match))
return best_match if confidence >= self.threshold else Non
重點看第二步:計算模板匹配的結果矩陣res,res = self._get_template_result_matrix()
def _get_template_result_matrix(self):
"""求取模板匹配的結果矩陣."""
# 灰度識別: cv2.matchTemplate( )只能處理灰度圖片參數
s_gray, i_gray = img_mat_rgb_2_gray(self.im_search), img_mat_rgb_2_gray(self.im_source)
return cv2.matchTemplate(i_gray, s_gray, cv2.TM_CCOEFF_NORMED)
可以看到最終用的是openCV的方法,cv2.matchTemplate,那個優先匹配上就返回結果。
4 總結
使用過程中可以發現Airtest框架有兩個缺點:一是對於背景透明的按鈕或者控制項,識別難度大;二是無法獲取文本內容,但這一缺點可通過引入文字識別庫解決,如:pytesseract。
對不能用UI控制項定位的部件,使用圖像識別定位還是非常方便的。UI自動化腳本編寫過程中可以將幾個框架結合使用,uiautomator定位速度較快,但對於flutter語言寫的頁面經常有一些部件無法定位,此時可以引入airtest框架用圖片進行定位。每個框架都有優劣勢,組合使用才能更好的實現目的。
作者:京東物流 範文君
來源:京東雲開發者社區