Android自動化測試探索(四)uiautomator2簡介和使用


uiautomator2簡介

 

項目Git地址: https://github.com/openatx/uiautomator2

 

安裝

#1. 安裝 uiautomator2

使用pip進行安裝, 注意因為uiaotumator2還在開發中需要加上--pre來安裝最新的版本

pip install --upgrade --pre uiautomator2

也可以使用源碼來進行安裝

git clone https://github.com/openatx/uiautomator2
pip install -e uiautomator2

安裝pillow庫, 截屏功能會需要用到這個庫

pip install pillow

#2.安裝守護程序到設備上(對設備進行初始化)

電腦連接上一個手機或多個手機, 確保adb已經添加到環境變量中,執行下面的命令會自動安裝本庫所需要的設備端程序:uiautomator-server 、atx-agentopenstf/minicapopenstf/minitouch

執行之前需要確保adb service開啟了

adb devices

執行上面的指令能看到連接的設備id就可以

# init 所有的已經連接到電腦的設備
python -m uiautomator2 init

如果沒有開啟adb service會提示以下錯誤, 開啟adb service服務就好了

socket.error: [Errno 61] Connection refused

安裝提示success即可, 注意安裝的過程中手機上會有提示是否安裝, 要手動確認下

#3. 安裝weditor (UI Inspector)

基於瀏覽器技術開發的weditor UI查看器, 安裝方法

pip install -U weditor

啟動方式:

python -m weditor

會自動打開瀏覽器,輸入設備的ip或者序列號,點擊Connect成功后,點擊刷新。

鼠標選擇控件,可以在右邊看到對應的屬性值

 使用

與設備建立連接

#1. 通過WiFi連接設備

如果手機和電腦處於同一個局域網, 可以使用如下方式通過手機ip建立連接

import uiautomator2 as u2

d = u2.connect('10.234.12.104')
print d.info

其中'10.234.12.104'是手機的IP地址, 可以通過adb指令獲得: adb shell ifconfig | grep Mask

運行結果如下:

{u'displayRotation': 0, u'displaySizeDpY': 829, u'displaySizeDpX': 393, u'screenOn': True, u'displayWidth': 1080, u'productName': u'lotus', u'currentPackageName': u'com.miui.home', u'sdkInt': 27, u'displayHeight': 2150, u'naturalOrientation': True}

#2.通過USB連接設備

如果手機與電腦有通過USB連接, 可以使用如下方式建立連接

import uiautomator2 as u2

d = u2.connect('62ab58430211')
print d.info

其中'62ab58430211'是手機的SN, 可以使用adb指令獲得: adb devices

運行結果如下:

{u'displayRotation': 0, u'displaySizeDpY': 829, u'displaySizeDpX': 393, u'screenOn': True, u'displayWidth': 1080, u'productName': u'lotus', u'currentPackageName': u'com.miui.home', u'sdkInt': 27, u'displayHeight': 2150, u'naturalOrientation': True}

#3.通過adb WiFi連接

如果配置了手機指定端口監聽TCP/IP連接, 比如

adb tcpip 5555

可以通過指定端口建立連接

import uiautomator2 as u2

d = u2.connect('10.234.12.104:5555')
print d.info

這個方法在我的機器上python2.7會報錯, 可能要在3.0以上

命令行指令

注: 下面的$device_ip代表手機IP

#1. init 為設備安裝所需要的程序

python -m uiautomator2 init

#2. install: 安裝apk, apk通過URL給出

python -m uiautomator2.cli install $device_ip https://example.org/some.apk

#3. clear-cache: 清空緩存

python -m uiautomator2 clear-cache

#4. app-stop-all: 停止所有應用

python -m uiautomator2 app-stop-all $device_ip

#5. screenshot: 截圖

python -m uiautomator2 screenshot $device_ip screenshot.jpg

#6. healthcheck: 健康檢查

python -m uiautomator2 healthcheck $device_ip

常用API

全局設置

#1. Debug HTTP Requests

import uiautomator2 as u2

d = u2.connect('10.234.12.104')
d.debug=True
print d.info

運行結果:

23:17:15.628 $ curl -X POST -d '{"params": {}, "jsonrpc": "2.0", "id": "eaacec696e5911b38ea35b652f5a0d54", "method": "deviceInfo"}' 'http://10.234.12.104:7912/jsonrpc/0'
23:17:15.856 Response (228 ms) >>>
{"jsonrpc":"2.0","id":"eaacec696e5911b38ea35b652f5a0d54","result":{"currentPackageName":"com.miui.home","displayHeight":2150,"displayRotation":0,"displaySizeDpX":393,"displaySizeDpY":829,"displayWidth":1080,"productName":"lotus","screenOn":true,"sdkInt":27,"naturalOrientation":true}}
<<< END
{u'displayRotation': 0, u'displaySizeDpY': 829, u'displaySizeDpX': 393, u'screenOn': True, u'displayWidth': 1080, u'productName': u'lotus', u'currentPackageName': u'com.miui.home', u'sdkInt': 27, u'displayHeight': 2150, u'naturalOrientation': True}

#2.Implicit wait

設置元素操作等待時間, 單位: 秒

d.implicitly_wait(10.0)
d(text="小米體檢").click()
print("wait timeout", d.implicitly_wait())

第一步為設置全局元素操作等待時間, 第二步點擊文本"小米體檢", 如果10秒內"小米體檢還沒有出現則會 raise UiObjectNotFoundError

這是設置會影響的操作有: click, long_click, drag_to, get_text, set_text, clear_text等

 

APP管理

#1. 安裝APP

只支持從網絡鏈接安裝

d.app_install('http://some-domain.com/some.apk')

#2. 運行APP

d.app_start("com.example.hello_world") # start with package name

 #3. 停止運行

# equivalent to `am force-stop`, thus you could lose data
d.app_stop("com.example.hello_world") 
# equivalent to `pm clear`
d.app_clear('com.example.hello_world')

 #4. 停止所有運行的app

# stop all
d.app_stop_all()
# stop all app except for com.examples.demo
d.app_stop_all(excludes=['com.examples.demo'])

 #5. 獲取APP信息

print d.app_info("com.examples.demo")

 #6. 保存app圖標

# save app icon
img = d.app_icon("com.examples.demo")
img.save("icon.png")

#7. Push文件到設備

# push to a folder
d.push("foo.txt", "/sdcard/")
# push and rename
d.push("foo.txt", "/sdcard/bar.txt")
# push fileobj
with open("foo.txt", 'rb') as f:
    d.push(f, "/sdcard/")
# push and change file access mode
d.push("foo.sh", "/data/local/tmp/", mode=0o755)

#8. 從設備pull文件

d.pull("/sdcard/tmp.txt", "tmp.txt")

# FileNotFoundError will raise if the file is not found on the device
d.pull("/sdcard/some-file-not-exists.txt", "tmp.txt")

 #9. 檢查並維持設備端守護進程處於運行狀態

d.healthcheck()

 

基本API使用

Shell Command

adb_shell已經廢棄,現在使用shell.

short-lived shell command

 

默認的超時為60s, 我們試試用shell命令發送pwd指令

output, exit_code = d.shell("pwd", timeout=60)
print output
print exit_code

 輸出:

/

0

 也可以這樣寫

output = d.shell('pwd').output
exit_code = d.shell('pwd').exit_code

 參數可以以list的形式使用,我們試試

output, exit_code = d.shell(['ls', '-l'])
print output
print exit_code

 long-running shell command

r = d.shell("logcat", stream=True)
# r: requests.models.Response
deadline = time.time() + 10 # run maxium 10s
try:
    for line in r.iter_lines(): # r.iter_lines(chunk_size=512, decode_unicode=None, delimiter=None)
        if time.time() > deadline:
            break
        print("Read:", line.decode('utf-8'))
finally:
    r.close() # this method must be called

 

Session

#1. 使用session來操作APP的開啟和關閉

sess = d.session("com.netease.cloudmusic") # start 網易雲音樂
sess.close() # 停止網易雲音樂

#2. 使用with來開啟和關閉

with d.session("com.netease.cloudmusic") as sess:
    sess(text="Play").click()

 #3. 創建已經打開的APP的session

sess = d.session('com.ganji.android.haoche_c', attach=True)
time.sleep(5)
sess.close()

 #4. 偵測APP Crash

# When app is still running
sess(text="Music").click() # operation goes normal

# If app crash or quit
sess(text="Music").click() # raise SessionBrokenError
# other function calls under session will raise SessionBrokenError too

 #5. 檢查session是否正常

# When app is still running
sess(text="Music").click() # operation goes normal

# If app crash or quit
sess(text="Music").click() # raise SessionBrokenError
# other function calls under session will raise SessionBrokenError too

 檢索設備信息

#1. base information

print d.info

 結果:

{u'displayRotation': 0, u'displaySizeDpY': 829, u'displaySizeDpX': 393, u'screenOn': True, u'displayWidth': 1080, u'productName': u'lotus', u'currentPackageName': u'com.miui.home', u'sdkInt': 27, u'displayHeight': 2150, u'naturalOrientation': True}

Process finished with exit code 0

#2. windows size

print(d.window_size())
# device upright output example: (1080, 1920)
# device horizontal output example: (1920, 1080)

#3. Get current app info

print d.current_app()

 運行結果:

{'activity': u'com.ganji.android.haoche_c.ui.main.MainActivity', 'package': u'com.ganji.android.haoche_c'}

Process finished with exit code 0

 #4. Wait activity

sess = d.session('com.ganji.android.haoche_c')
time.sleep(5)
print d.wait_activity('com.ganji.android.haoche_c.ui.main.MainActivity')

 #5. Get device serial number

print d.serial

 #6. Get WLAN ip

print d.wlan_ip

 #7. Get detailed device info

print d.device_info

 

Key Events

#1. Turn on/off screen

d.screen_on() # turn on the screen
d.screen_off() # turn off the screen

#2. Get current screen status

print d.info.get('screenOn')

#3. Press hard/soft key

d.press("home") # press the home key, with key name
d.press("back") # press the back key, with key name
d.press(0x07, 0x02) # press keycode 0x07('0') with META ALT(0x02)

 之前支持的key有以下這些:

home
back
left
right
up
down
center
menu
search
enter
delete ( or del)
recent (recent apps)
volume_up
volume_down
volume_mute
camera
power

 更多key可以查看: Android KeyEvnet

 #4. Unlock screen

d.unlock()
# This is equivalent to
# 1. launch activity: com.github.uiautomator.ACTION_IDENTIFY
# 2. press the "home" key

 

Gesture interaction with the device

 #1. Click on the screen

d.click(x, y)

#2. Double click

d.double_click(x, y)
d.double_click(x, y, 0.1) # default duration between two click is 0.1s

#3. Long click on the screen

d.long_click(x, y)
d.long_click(x, y, 0.5) # long click 0.5s (default)

 

 #4. Swipe

d.swipe(sx, sy, ex, ey)
d.swipe(sx, sy, ex, ey, 0.5) # swipe for 0.5s(default)

 

 #5. Drag

d.drag(sx, sy, ex, ey)
d.drag(sx, sy, ex, ey, 0.5) # swipe for 0.5s(default)

 

 #6. Swipe points

# swipe from point(x0, y0) to point(x1, y1) then to point(x2, y2)
# time will speed 0.2s bwtween two points
d.swipe((x0, y0), (x1, y1), (x2, y2), 0.2)

 

 多用於九宮格解鎖,提前獲取到每個點的相對坐標(這里支持百分比), 更詳細的使用參考這個帖子 使用u2實現九宮圖案解鎖

 #7. Touch and drap (beta)

這個接口屬於比較底層的原始接口,感覺並不完善,不過湊合能用。注:這個地方並不支持百分比

d.touch.down(10, 10) # 模擬按下
time.sleep(.01) # down 和 move 之間的延遲,自己控制
d.touch.move(15, 15) # 模擬移動
d.touch.up() # 模擬抬起

 

click, swipe, drag操作支持按比例操作, 比如

d.long_click(0.5, 0.5)

意思是長按屏幕中心

 

Screen-Related

#1. 獲取當前設備方向

print d.orientation

 可能的方向有

natural or n
left or l
right or r
upsidedown or u (can not be set)

#2. 設置設備方向

d.set_orientation('l') # or "left"
d.set_orientation("l") # or "left"
d.set_orientation("r") # or "right"
d.set_orientation("n") # or "natural"

#3. 禁止旋轉和解除

# freeze rotation
d.freeze_rotation()
# un-freeze rotation
d.freeze_rotation(False)

#4. 截屏

# take screenshot and save to a file on the computer, require Android>=4.2.
d.screenshot("home.jpg")

# get PIL.Image formatted images. Naturally, you need pillow installed first
image = d.screenshot() # default format="pillow"
image.save("home.jpg") # or home.png. Currently, only png and jpg are supported

# get opencv formatted images. Naturally, you need numpy and cv2 installed first
import cv2
image = d.screenshot(format='opencv')
cv2.imwrite('home.jpg', image)

# get raw jpeg data
imagebin = d.screenshot(format='raw')
open("some.jpg", "wb").write(imagebin)

#5. 獲取UI層級關系

# get the UI hierarchy dump content (unicoded).
xml = d.dump_hierarchy()

#6. 打開通知中心

d.open_notification()

#7. 快速設置

d.open_quick_settings()

Selector

 選擇當前窗口中的UI控件, 例如

# Select the object with text 'Clock' and its className is 'android.widget.TextView'
d(text='Clock', className='android.widget.TextView')

支持以下這些選擇參數, 詳細可以參考UiSelector Java doc

text, textContains, textMatches, textStartsWith
className, classNameMatches
description, descriptionContains, descriptionMatches, descriptionStartsWith
checkable, checked, clickable, longClickable
scrollable, enabled,focusable, focused, selected
packageName, packageNameMatches
resourceId, resourceIdMatches
index, instance

 #1. Children (子級控件)

# get the children or grandchildren
d(className="android.widget.ListView").child(text="Bluetooth")

 #2. Siblings(同級控件)

# get siblings
d(text="Google").sibling(className="android.widget.ImageView")

#3. children by text or description or instance

# get the child matching the condition className="android.widget.LinearLayout"
# and also its children or grandchildren with text "Bluetooth"
d(className="android.widget.ListView", resourceId="android:id/list") \
 .child_by_text("Bluetooth", className="android.widget.LinearLayout")

# get children by allowing scroll search
d(className="android.widget.ListView", resourceId="android:id/list") \
 .child_by_text(
    "Bluetooth",
    allow_scroll_search=True,
    className="android.widget.LinearLayout"
  )
  • child_by_description is to find children whose grandchildren have the specified description, other parameters being similar to child_by_text.

  • child_by_instance is to find children with has a child UI element anywhere within its sub hierarchy that is at the instance specified. It is performed on visible views without scrolling.

See below links for detailed information:

  • UiScrollablegetChildByDescriptiongetChildByTextgetChildByInstance
  • UiCollectiongetChildByDescriptiongetChildByTextgetChildByInstance

Above methods support chained invoking, e.g. for below hierarchy

<node index="0" text="" resource-id="android:id/list" class="android.widget.ListView" ...>
  <node index="0" text="WIRELESS & NETWORKS" resource-id="" class="android.widget.TextView" .../>
  <node index="1" text="" resource-id="" class="android.widget.LinearLayout" ...>
    <node index="1" text="" resource-id="" class="android.widget.RelativeLayout" ...>
      <node index="0" text="Wi‑Fi" resource-id="android:id/title" class="android.widget.TextView" .../>
    </node>
    <node index="2" text="ON" resource-id="com.android.settings:id/switchWidget" class="android.widget.Switch" .../>
  </node>
  ...
</node>

 

 

 

To click the switch widget right to the TextView 'Wi‑Fi', we need to select the switch widgets first. However, according to the UI hierarchy, more than one switch widgets exist and have almost the same properties. Selecting by className will not work. Alternatively, the below selecting strategy would help:

d(className="android.widget.ListView", resourceId="android:id/list") \
  .child_by_text("Wi‑Fi", className="android.widget.LinearLayout") \
  .child(className="android.widget.Switch") \
  .click()

 #4. relative positioning

我們可以使用相對位置來輔助獲取控件: left、right、top、bottom.

  • d(A).left(B), selects B on the left side of A.
  • d(A).right(B), selects B on the right side of A.
  • d(A).up(B), selects B above A.
  • d(A).down(B), selects B under A.

所以上面的例子可以改為:

## select "switch" on the right side of "Wi‑Fi"
d(text="Wi‑Fi").right(className="android.widget.Switch").click()

#5. Multiple instances

有時候我們會遇到屏幕上有多個控件的屬性一樣,這個時候就可以使用instance屬性來獲取其中一個控件

d(text="Add new", instance=0)  # which means the first instance with text "Add new"

另外還提供了以下API功能

# get the count of views with text "Add new" on current screen
d(text="Add new").count

# same as count property
len(d(text="Add new"))

# get the instance via index
d(text="Add new")[0]
d(text="Add new")[1]
...

# iterator
for view in d(text="Add new"):
    view.info  # ...

Notes: when using selectors in a code block that walk through the result list, you must ensure that the UI elements on the screen keep unchanged. Otherwise, when Element-Not-Found error could occur when iterating through the list.

Get the selected ui object status and its information

#1. 判斷UI控件是否存在

d(text="Settings").exists # True if exists, else False
d.exists(text="Settings") # alias of above property.

# advanced usage
d(text="Settings").exists(timeout=3) # wait Settings appear in 3s, same as .wait(3)

#2. 獲取控件信息

print d(text="Settings").info

輸出:

{ u'contentDescription': u'',
u'checked': False,
u'scrollable': False,
u'text': u'Settings',
u'packageName': u'com.android.launcher',
u'selected': False,
u'enabled': True,
u'bounds': {u'top': 385,
            u'right': 360,
            u'bottom': 585,
            u'left': 200},
u'className': u'android.widget.TextView',
u'focused': False,
u'focusable': True,
u'clickable': True,
u'chileCount': 0,
u'longClickable': True,
u'visibleBounds': {u'top': 385,
                    u'right': 360,
                    u'bottom': 585,
                    u'left': 200},
u'checkable': False
}

#3. 獲取/輸入/清空文本

d(text="Settings").get_text()  # get widget text
d(text="Settings").set_text("My text...")  # set the text
d(text="Settings").clear_text()  # clear the text

#4. 獲取控件中心坐標/中心偏移坐標

x, y = d(text="Settings").center()
# x, y = d(text="Settings").center(offset=(0, 0)) # left-top x, y 

Perform the click action on the selected UI object

#1. 點擊控件

# click on the center of the specific ui object
d(text="Settings").click()

# wait element to appear for at most 10 seconds and then click
d(text="Settings").click(timeout=10)

# click with offset(x_offset, y_offset)
# click_x = x_offset * width + x_left_top
# click_y = y_offset * height + y_left_top
d(text="Settings").click(offset=(0.5, 0.5)) # Default center
d(text="Settings").click(offset=(0, 0)) # click left-top
d(text="Settings").click(offset=(1, 1)) # click right-bottom

# click when exists in 10s, default timeout 0s
clicked = d(text='Skip').click_exists(timeout=10.0)

# click until element gone, return bool
is_gone = d(text="Skip").click_gone(maxretry=10, interval=1.0) # maxretry default 10, interval default 1.0

#2. 長按控件

# long click on the center of the specific UI object
d(text="Settings").long_click()

Gesture actions for the specific UI object

#1. 拖動控件往指定坐標或控件

# notes : drag can not be used for Android<4.3.
# drag the UI object to a screen point (x, y), in 0.5 second
d(text="Settings").drag_to(x, y, duration=0.5)
# drag the UI object to (the center position of) another UI object, in 0.25 second
d(text="Settings").drag_to(text="Clock", duration=0.25)

#2. 控件上滑動

d(text="Settings").swipe("right")
d(text="Settings").swipe("left", steps=10)
d(text="Settings").swipe("up", steps=20) # 1 steps is about 5ms, so 20 steps is about 0.1s
d(text="Settings").swipe("down", steps=20)

#3. 雙指手勢

d(text="Settings").gesture((sx1, sy1), (sx2, sy2), (ex1, ey1), (ex2, ey2))

#4. 縮放手勢

# notes : pinch can not be set until Android 4.3.
# from edge to center. here is "In" not "in"
d(text="Settings").pinch_in(percent=100, steps=10)
# from center to edge
d(text="Settings").pinch_out()

#5. 等待控件出現或消失

# wait until the ui object appears
d(text="Settings").wait(timeout=3.0) # return bool
# wait until the ui object gone
d(text="Settings").wait_gone(timeout=1.0)

The default timeout is 20s. see global settings for more details

#6. 慣性滾動控件

# fling forward(default) vertically(default) 
d(scrollable=True).fling()
# fling forward horizontally
d(scrollable=True).fling.horiz.forward()
# fling backward vertically
d(scrollable=True).fling.vert.backward()
# fling to beginning horizontally
d(scrollable=True).fling.horiz.toBeginning(max_swipes=1000)
# fling to end vertically
d(scrollable=True).fling.toEnd()

#7. 滾動控件

# scroll forward(default) vertically(default)
d(scrollable=True).scroll(steps=10)
# scroll forward horizontally
d(scrollable=True).scroll.horiz.forward(steps=100)
# scroll backward vertically
d(scrollable=True).scroll.vert.backward()
# scroll to beginning horizontally
d(scrollable=True).scroll.horiz.toBeginning(steps=100, max_swipes=1000)
# scroll to end vertically
d(scrollable=True).scroll.toEnd()
# scroll forward vertically until specific ui object appears
d(scrollable=True).scroll.to(text="Security")

 

 全局變量

# set delay 1.5s after each UI click and click
d.click_post_delay = 1.5 # default no delay

# set default element wait timeout (seconds)
d.wait_timeout = 30.0 # default 20.0

超時設置

>> d.jsonrpc.getConfigurator() 
{'actionAcknowledgmentTimeout': 500,
 'keyInjectionDelay': 0,
 'scrollAcknowledgmentTimeout': 200,
 'waitForIdleTimeout': 0,
 'waitForSelectorTimeout': 0}

>> d.jsonrpc.setConfigurator({"waitForIdleTimeout": 100})
{'actionAcknowledgmentTimeout': 500,
 'keyInjectionDelay': 0,
 'scrollAcknowledgmentTimeout': 200,
 'waitForIdleTimeout': 100,
 'waitForSelectorTimeout': 0}

為了防止客戶端程序響應超時,waitForIdleTimeoutwaitForSelectorTimeout目前已改為0

 

Input Method

這種方法通常用於不知道控件的情況下的輸入。第一步需要切換輸入法,然后發送adb廣播命令,具體使用方法如下

d.set_fastinput_ime(True) # 切換成FastInputIME輸入法
d.send_keys("你好123abcEFG") # adb廣播輸入
d.clear_text() # 清除輸入框所有內容(Require android-uiautomator.apk version >= 1.0.7)
d.set_fastinput_ime(False) # 切換成正常的輸入法
d.send_action("search") # 模擬輸入法的搜索

 

Toast

顯示Toast

d.toast.show("Hello world")
d.toast.show("Hello world", 1.0) # show for 1.0s, default 1.0s

獲取Toast

# [Args]
# 5.0: max wait timeout. Default 10.0
# 10.0: cache time. return cache toast if already toast already show up in recent 10 seconds. Default 10.0 (Maybe change in the furture)
# "default message": return if no toast finally get. Default None
d.toast.get_message(5.0, 10.0, "default message")

# common usage
assert "Short message" in d.toast.get_message(5.0, default="")

# clear cached toast
d.toast.reset()
# Now d.toast.get_message(0) is None

 

Stop UiAutomator

停止UiAutomator守護服務

https://github.com/openatx/uiautomator2/wiki/Common-issues

因為有atx-agent的存在,Uiautomator會被一直守護着,如果退出了就會被重新啟動起來。但是Uiautomator又是霸道的,一旦它在運行,手機上的輔助功能、電腦上的uiautomatorviewer 就都不能用了,除非關掉該框架本身的uiautomator。下面就說下兩種關閉方法

方法1:

直接打開uiautomator app(init成功后,就會安裝上的),點擊關閉UIAutomator

方法2:

d.service("uiautomator").stop()

# d.service("uiautomator").start() # 啟動
# d.service("uiautomator").running() # 是否在運行

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM