Coding起來-自動化測試: Python + UIAutomator 搭配UIAutomatorViewer-模擬手點操作手機
嗨嗨,今天來跟大家介紹一個我最近學習的Android手機自動化測試非常好用的工具UIAutomator,在學習它之前,我都是使用Python直接呼叫adb指令,完成自動化手機操作,但這個工具真的相當直覺好用,所以就來學習記錄一下,這篇前半段的教學內容來自於xiaocong的Github大作,真的非常厲害,加入自己的筆記跟見解後,紀錄成一篇教學文,寫完才發現UIAutomator2已經出來了, 但語法和功能上應該是大同小異, 有時間的話會在記錄一篇給大家參考
UIAutomator是什麼呢?
簡單來說,它是由Google推出的一個對Andorind UI進行測試的工具,它可以模擬手點的方式進行一系列的自動化測試,更貼近使用者的使用行為,任何的操作都能檢驗是否符合預期,是個非常強大好用的測試工具
Python + UIAutomator?
UIAutomator的腳本基本上是由Java寫的,而xiaocong等人製作了一個Python的橋樑,讓我們可以使用Python來使用UIAutomator
直接來操作,大家就會理解了
Step 1: 下載套件
1. 我這邊使用Anaconda Prompt 來下載uiautomator套件
pip install uiautomator
Step 2: 連接手機與PC
1. 我使用adb來進行連接,它是下載SDK Platform Tools (https://developer.android.com/studio/releases/platform-tools.html)就會夾帶的工具喔
2. 下載好後,設定為環境變數,並在cmd裡測試,只要打上adb devices,結果有device的serial number出來就表示連接完成
Step 3: 導入套件
1. 只有一台手機連接的狀況
from uiautomator import device as d
2. 多台手機連接時,需要指定視哪一手機,要指定手機的Serial Number
from uiautomator import Device d = Device(‘13cad2f9’) ## (Serial Number)
3. 如果手機連接在另一台電腦,你需要知道手機設備Serial Number、對方ip位置與port
from uiautomator import Device d = Device(‘13cad2f9’, adb_server_host=’192.168.1.68',adb_server_port=5037)
小筆記: 1. 我是用adb devices 指令來知道我的手機Serial Number
Step 4: 操作指令介紹
1. 常用的API
1–1. 獲取手機的資訊
print(d.info)
結果:
{‘currentPackageName’: ‘XXX’, ‘displayHeight’: 2016, ‘displayRotation’: 0, ‘displaySizeDpX’: 360, ‘displaySizeDpY’: 720, ‘displayWidth’: 1080, ‘productName’: 'XXX’, ‘screenOn’: True, ‘sdkInt’: XX, ‘naturalOrientation’: True}
補充: 結果的各項值解釋
- displayRotation: 0(代表直屏) 或 1(代表橫屏)
- currentPackageName: 現在活動(Activity)的套件(Package)名稱
- productName: 現在的設備名稱
- displayHeight: 現在的螢幕寬度
- displayWidth: 現在的螢幕高度
- sdkInt: 現在的SDK版本
- naturalOrientation: 橫屏的時候是Flase, 直屏的時候是True
1–2. 螢的操作 1:
a. 點亮與關掉螢幕
## 點亮螢幕 d.screen.on() d.wakeup() ## 關掉螢幕 d.screen.off() d.sleep()
小筆記: 實際使用時,可以加上time.sleep(),不然操作速度過快,可能會看不出來或啟用到別的功能(可能被判定為同時按兩個按鍵)
b. 在自動化程式中加入條件,判斷螢幕狀況,非常實用
## 如果螢幕開著,就print一個yes if d.screen == “on”: print(‘yes’) ## 如果螢幕開著,就print一個no elif d.screen == “off”: print(‘no’)
1–3 按鍵操作
a. 支援如下的按鍵
home back menu search enter recent volume_up volume_down volume_mute camera power left right up down center
b. 操作示範
## 查看哪些App正在使用 d.press.recent() d.press(“recent”) ## 按下power鍵 d.press.power() d.press(“power”) ## 按下相機 d.press.camera() ## 按home鍵 d.press.home() d.press(“home”) ## 按返回鍵 d.press.back() d.press(“back”) ## 按下關鍵代碼 d.press(0x07, 0x02) ## 按下搜尋 d.press.search()
c. 獲得更多的操作方式,參考官網
1–4 模擬手點操作:
點擊螢幕座標上的位置(x,y)
a. 短按點擊
## d.click(x,y) d.click(200,400)
小筆記: d.click(200,400),相當於adb指令: adb shell input tap 200 400
b. 長按點擊
## d.long_click(x, y) d.long_click(200,400)
c. 滑動操作
## d.swipe(sx, sy, ex, ey) d.swipe(800, 400, 100, 400)
小筆記: d.swipe(800, 400, 100, 400),相當於adb指令: adb shell input swipe 800 400 100 400
d. 搬動操作
##steps: 幾步移動到 ## d.drag(sx, sy, ex, ey, steps=) ## 將app 搬動到另一個位置 d.drag(200,400, 800, 400) ## 將app 搬動到另一個位置, 十步內移到 d.drag(200,400, 800, 400, steps=10)
1–5: 螢幕操作 2:
a. 螢幕旋轉方向設定
## 恢復原設定方向 orientation = d.orientation ## 旋轉方向設定 d.orientation = “left” ## 或者寫 l d.orientation = “right” ## 或者寫 r d.orientation = “natural” ## 或者寫 n d.orientation = “upsidedown”
b. 鎖定或解鎖旋轉功能
## 鎖定旋轉 d.freeze_rotation() ## 打開旋轉 d.freeze_rotation(False)
c. 螢幕截圖
## screenshot and save to file d.screenshot(“test.png”) ## 前面可以加上想放在的目錄路徑,像是d.screenshot(“C:/Users/user/Desktop/test.png”)
d. 取得螢幕層級資訊(XML)
## 取得螢幕層級資訊, 儲存於local的檔案目錄路徑下d.dump(“screen_hierachy.xml”) ## 取得螢幕層級資訊,印出取得的資訊 xml = d.dump() print(xml)
e. 打開便捷設定欄與通知欄
## 打開通知欄 d.open.notification() ## 打開便捷設定欄 d.open.quick_settings()
f. 螢幕狀態
## 等待窗口閒置 d.wait.idle() ## 等待窗口更新 d.wait.update()
1–6 簡單的實作: 為了讓大家有點感覺,我們來實作一個小小的demo
流程: 檢查螢幕是否點亮->沒點亮的話就點亮它->滑到第二頁->將左上角的APP搬動到另一個位置->回到主頁
## 導入套件 import time from uiautomator import device as d ## 先檢查螢幕是否點亮,如果沒有就點亮它 ## 如果螢幕開著,就print一個yes if d.screen == “on”: pass ## 如果螢幕開著,就print一個no else: d.screen.on() ## 移動到第二頁 d.swipe(800, 400, 50, 400) ## 等待一下time.sleep(2) ## 把第二頁的第一個app位置搬動道別的位置 d.drag(200,400, 800, 800, steps=10) ## 回到首頁 d.press.home()
2. Watcher 用法
簡單來說,就是讓我們加入一些條件,如果條件成立,那就會執行我們設定的指令,而它的條件使用的是tag的方法,符合有哪個tag就執行
補充:
i. .when(a, b): 執行條件符合a和b的狀況
ii. 如何知道tag有哪些, 取得階層,就可以找到對應的了,有點像html
xml = d.dump() print(xml)
iii. 或是使用UIAutomatorViewer輕鬆獲取階層資訊,文章下面會介紹到如何使用
iii. 下面的範例: 我是將手機點開電話這個app,然後進行指令的
a. 用法 1
## 設定一個watcher, 當tag裡面出現text=”Contacts”和text=”Recents”,就點擊Contacts d.watcher(“test_press_phone_app_contacts”).when(text=”Contacts”).when(text=”Recents”) \ .click(text =”Contacts”) ## 執行所有 watcherd.watchers.run()
b. 用法 2
## 當有符合text=”Contacts”和selected=”true”,就點擊電源鍵 d.watcher(“test_press_phone_app_power”).when(text=”Contacts”,selected=”true”) \ .press.power() ## 執行所有 watcherd.watchers.run()
c. 查看是否被觸發了
## i. 指定是哪一個watcher的時候,輸入watcher的名稱print(d.watcher(“test_press_phone_app_power”).triggered) ## ii. 只要任何一個watcher被觸發就算 print(d.watchers.triggered)
d. 移除掉Watcher
##i. 指定移除哪個watcher d.watchers.remove() ##ii. 移除掉所有watcherd.watchers.remove(“test_press_phone_app_power”)
e. 例出所有存在的Watcher
print(d.watchers)
f. Reset 被觸發過的Wacther: 這樣d.watchers.triggered就會reset回false
d.watchers.reset()
3. Handler 用法
它可以使用callback function,和Watcher非常相似
def handler_test(device): if device(text=”Contacts”).exists: device.press.power() device(text=”Contacts”).click() return True ## 開啟 callback function d.handlers.on(handler_test) ## 關閉 callback function d.handlers.off(handler_test)
小筆記: 唯獨這個用法我試過,沒有報錯,但是也沒有work的感覺,如果有哪位高手發現問題,歡迎留言教導我,哈哈
4. Selector 用法
Selector 是用來識別螢幕的object,它利用下例的屬性來辨別
a. text, textContains, textMatches, textStartsWith b. className, classNameMatches c. description, descriptionContains, descriptionMatches, descriptionStartsWith d. checkable, checked, clickable, longClickablee. scrollable, enabled,focusable, focused, selected f. packageName, packageNameMatches g. resourceId, resourceIdMatches h. index, instance
舉例: 取得一個object, 條件: text = “”, 然後它的className= “”
d(text=”Phone”, className=’android.phone’)
4–1: 透過是親子(child)或是兄弟(sibling)關係
a. child
## 取得子屬性 d(className=’android.phone’).child(text=”Contacts”)
b. sibling
d(text=”Contacts”).sibling(text=”Recents”)
c. child_by_text
利用child_by_text這個方法來取得,allow_scroll_search=True,允許自動化程式自己下拉找尋子屬性,還有child_by_description和child_by_instance用法,可以參考連結
## 找尋一個text=text=”Phone”和className=’android.phone’的child,然後找尋它的child,符合text= “Contacts”, className=”Contacts_class_name” d(text=”Phone”, className=’android.phone’) \ .child_by_text(“Contacts”, className=”Contacts_class_name”) # 允許下拉查詢 d(text=”Phone”, className=’android.phone’) \ .child_by_text( “Contacts”, allow_scroll_search=True, className=”Contacts_class_name” )
4–2: 透過相對位置來找尋選擇, 像是up/right/dwon/left
1. 範例中: 會把Messaging左邊的Phone(App)點擊打開來
## 選擇在text=”Phone”左邊,且text=”Phone”的物件,然後點擊 d(text=”Messaging”).left(text=”Phone”).click()
4–3: 得到手機UI相關的信息
小筆記: 需在有Settings這個App的畫面
## 獲得UI資訊 print(d(text=”Settings”).info) ## 檢查現在UI資訊是否存在 print(d(text=”Settings”).exists) print(d.exists(text=”Settings”))
4–4: 設定與清理掉可以編輯的文本內容
## 設定 d(text=”Settings”).set_text(“NEW TEXT FOR TESTING…”) ## 清除掉 d(text=”Setiings”).clear_text()
4–5 對UI有屬性text=”Settings”的物件上點擊不同位置
## 點擊物件右下角的位置 d(text=”Settings”).click.bottomright() ## 點擊物件左上角的位置 d(text=”Settings”).click.topleft() ## 點擊物件後等待頁面開好 d(text=”Settings”).click.wait()
4–6 對UI有屬性text=”Settings”的物件上長按點擊不同位置
## 長按點擊 d(text=”Settings”).long_click() ## 長按點擊物件右下角的位置 d(text=”Settings”).long_click.bottomright() ## 長按點擊物件左上角的位置 d(text=”Settings”).long_click.topleft()
4–7 搬動Object位置
## 將指定的Object搬動到指定的x軸y軸,用n個steps完成 ## d(text= “Settings”).drag.to(x, y, steps=10) d(text= “Settings”).drag.to(400, 800, steps=100) ## 搬運到另一個指定的Object位置 d(text= “Settings”).drag.to(text= “Clock”, steps=100)
4–8 在指定UI上的物件上進行滑動, 有up/right/down/left
d(text=”Settings”).swipe.right() d(text=”Settings”).swipe.left(steps=10) d(text=”Settings”).swipe.up(steps=20) d(text=”Settings”).swipe.down(steps=20)
4–9 同時多指操作
a. 雙指操作
## d(text=”Settings”).gesture((sx1, sy1), (sx2, sy2)) \## .to((ex1, ey1), (ex2, ey2)) d(text=”Settings”).gesture((400, 400), (200, 400)) \ .to((500, 500), (300, 500))
b. 雙指操作, 兩手指往裡面捏的動作 跟往外面撥的動作
## 往裡面捏 d(text=”Settings”).pinch.In(percent=100, steps=20) ## 往外面撥 d(text=”Settings”).pinch.Out(percent=100, steps=20)
c. 三指操作
# d().gestureM((sx1, sy1), (sx2, sy2),(sx3, sy3)) \# .to((ex1, ey1), (ex2, ey2),(ex3,ey3)) d().gestureM((200,100),(400,600),(700,200),(100,800),(300,600),(600,1000))
4–10 等待UI上指定的Object消失或出現,會返回True or False, 等待時間: timeout=4000
## 等待消失 print(d(text=”Phone”).wait.gone(timeout=4000)) ## 等待出現 print(d(text=”Phone”).wait.exists(timeout=4000))
4–11 各種滑動操作,有水平、垂直、向前、向後方向滑動
## 預設滑動方向,前進跟垂直 d(scrollable=True).fling() ## 滑動水平前進 d(scrollable=True).fling.horiz.forward() ## 滑動垂直後退 d(scrollable=True).fling.vert.backward() ## 開始往水平滑動d(scrollable=True).fling.horiz.toBeginning(max_swipes=1200) ## 結束往垂直滑動 d(scrollable=True).fling.vert.toEnd()
4–12 滾動操作, 有水平、垂直、向前、向後方向滾動
## 預設滾動方向: 向前、垂直 d(scrollable=True).scroll(steps=10) ## 往前水平滾動 d(scrollable=True).scroll.horiz.forward(steps=10) ## 往後垂直滾動 d(scrollable=True).scroll.vert.backward(steps=10) ## 開始往水平方向滾動d(scrollable=True).scroll.horiz.toBeginning(steps=400, max_swipes=1200) ## 結束往垂直方向滾動 d(scrollable=True).scroll.vert.toEnd() ## 往垂直方向滾動到指定的Object d(scrollable=True).scroll.to(text=”Phone”)
UIAutomatorViewer: 幫助我們方便查看層級關係(Hierarchy)的工具
使用UIAutomatorViewer可以清楚了解手機或模擬器上UI的階層關係)
- 下載Android Studio,但我們其實只需要它夾帶的uiautomatorviewer.bat
- 下載好後,先連接好手機,並到C:\Users\user\AppData\Local\Android\Sdk\tools\bin目錄底下( 通常是在這個目錄底下),點擊uiautomatorviewer.bat並執行
- 如果找不到uiautomatorviewer.bat, 可以從Android Studio裡面找到檔案目錄路徑(如下圖)
Step1:
a.開啟Android Studio,點擊Configure,按進SDK Manager
b. 或右上角也能進入到SDK Manager
Step 2: 找到指定目錄路徑
Step 3: 進入目錄路徑,找到uiautomatorviewer
Step 4: 打開後點擊右上方的按鈕(如下圖),就會顯示我們要的層級關係與細節資訊
希望這次的教學大家都有滿出來的收穫,可以開始自己寫自動化操控手機的程式了或針對開發好的App進行測試, 有問題都歡迎隨時找我討論喔!!
Reference:
https://github.com/xiaocong/uiautomator