MQC功能測試大揭秘(3)- Appium基礎篇
上文回顧
上一篇為大家介紹了如何通過appium桌麵客戶端的方式來快速搭建appium環境,桌麵客戶端的appium版本目前為1.6.4,更新稍慢於appium項目,但目前已經支持在線更新,大家不用再有客戶端版本過低的顧慮。
接下來將介紹如何使用python來開發appium功能測試腳本,包括啟動、控件定位、操作、函數封裝、組織用例五個部分。
啟動
Appium啟動時需要指定一些通用配置,統稱為Desired Capabilities,具體的一些參數可以參考Appium服務器初始化參數。這裏介紹一些通用的參數與一些常見的問題。
automationName
自動化測試的引擎,Appium (默認)、Selendroid、Uiautomator2。Appium使用的是UI Automator v1,相比之下UI Automator v2修複了一些v1的bug,在結構上也有一些優化。對於Android7.0以上的係統,UI Automator v1可能在查找控件時出現超時導致appium服務端報錯,這時候可以考慮改用Uiautomator2。
platformName
手機操作係統。
platformVersion
手機操作係統版本。
deviceName
手機類型
app
待測app的路徑
newCommandTimeout
兩條appium命令間的最長時間間隔,若超過這個時間,appium會自動結束並退出app
noReset, fullReset
noReset 不要在會話前重置應用狀態。默認值false。 fullReset (Android) 通過卸載而不是清空數據來重置應用狀態。在Android上, 這也會在會話結束後自動清除被測應用。默認值false。
unicodeKeyboard, resetKeyboard
在輸入的時候,可能出現鍵盤擋住控件的情況,這時候需要使用 appium 提供的輸入法(支持輸入多語言,沒有鍵盤 ui ),unicodeKeyboard 為 true 表示使用 appium-ime 輸入法。 resetKeyboard 表示在測試結束後切回係統輸入法。
appActivity, appPackage
appActivity與appPackage指用於啟動待測app的activityName與packageName,appium(1.6.4)已經支持activityName與packageName的自動檢測,這兩個參數已經可以省略了
appWaitActivity, appWaitPackage
appium需要等待的activityName與packageName,與appActivity不同的是,對於有啟動動畫的app來說,appWaitActivity應該是啟動activity消失後出現的activity。這兩個參數可以指定多個。
有了以上介紹的這些參數,我們可以啟動appium並開始測試app了。將desired capibilities進行封裝,python腳本如下:
from appium import webdriver
def get_desired_capabilities():
desired_caps = {
'platformName': 'Android',
'platformVersion': '18',
'deviceName': 'mqcDevice',
'udid': 'a05aacaf7d53',
'app': "D:\\appium\\alicrowdtest.apk",
'newCommandTimeout': 60,
'automationName': 'appium',
'unicodeKeyboard': True,
'resetKeyboard': True,
'appWaitActivity': 'com.yunos.mqc.view.activity.MainActivity',
}
return desired_caps
def setUp():
# 獲取我們設定的capabilities,通知Appium Server創建相應的會話。
desired_caps = get_desired_capabilities()
# 獲取server的地址。
uri = "https://localhost:4723/wd/hub"
# 創建會話,得到driver對象,driver對象封裝了所有的設備操作。下麵會具體講。
driver = webdriver.Remote(uri, desired_caps)
return driver
if __name__ == '__main__':
driver = setUp()
// 打印當前activity
print driver.current_activity
控件定位
android sdk的tools目錄下自帶一個元素查看工具-uiautomatorviewer,通過這個工具可以獲取到app的各個元素屬性,輔助我們編寫相關的腳本,uiautomatorviewer的界麵如下:
如圖為了定位 尚未登錄 這個控件,我們推薦以下幾種方法:
- xpath: xpath定位效率低,但勝在定位準確,在控件沒有明顯的唯一特征時,xpath的優勢就體現出來了。使用xpath時可以選擇以控件樹的最近公共祖先的節點開始生成查詢路徑。
driver.find_element_by_xpath("//android.widget.LinearLayout[1]/android.widget.FrameLayout[1]/android.widget.ScrollView[1]/android.widget.LinearLayout[1]/android.widget.RelativeLayout[1]/android.widget.LinearLayout[1]/1android.widget.TextView[1]")
- id: 為了定位方便,開發一般會為部分控件添加resource-id屬性,一般來說可以在一個頁麵中唯一確定一個控件,對於存在多個相同resource-id控件的頁麵,可以通過index來定位這些控件中的某一個
driver.find_element_by_id("com.yunos.mqc:id/user_nickname")
或者
driver.find_elements_by_id("com.yunos.mqc:id/user_nickname")[index]
- text: 定位控件較為簡潔清晰地一種方式,就是通過text來查找控件。
#1.5以上的版本已棄用
driver.find_element_by_name("尚未登錄")
#新版appium使用xpath來實現這個功能
driver.find_element_by_xpath("//*[@text='%s']" % (text))
操作
- swipe: 很多App都會有引導頁需要左滑,這裏提供一個左滑的例子,注意,滑動速度不應太快,否則容易導致引導頁滑動不成功。
window_size = driver.get_window_size()
driver.swipe(start_x=window_size["width"] * 0.9,
start_y=window_size["height"] * 0.5,
end_x=window_size["width"] * 0.1,
end_y=window_size["height"] * 0.5, 500)
- TouchAction: touchaction可以用來實現點擊坐標,滑動等操作,需要注意的是在android係統上,TouchAction的move_to函數是相對坐標。
#點擊操作
TouchAction(driver).press(None, x, y).release().perform()
#滑動操作
TouchAction(driver).press(None, x, y).wait(20).move_to(None, dx, dy).release().perform()
- Scroll: 對於ListView這樣的控件,若要滑動到某個控件位置,按照坐標滑動的方式很難適配不同分辨率的手機,這時候需要考慮使用scroll的方式進行滑動。
#從控件 el1 滑動到 el2
driver.scroll(el1, el2)
- Keyevent: 通過係統按鍵可以實現很多操作,如HOME、POWER、MENU、音量等功能。具體的一些keyevent可以到https://developer.android.google.cn/reference/android/view/KeyEvent.html查看。
#HOME 鍵
driver.keyevent(3)
- long_press: 利用TouchAction,可以實現長按操作:
#長按 el 控件 20s
action = TouchAction(driver)
action.long_press(el, 20000).perform()
sleep(20)
action.release().perform()
- hidekeyboard: 若 desiredcapbility 沒有指定使用 unicodeKeyboard,在輸入的時候需要注意鍵盤 ui 可能會擋住控件的情況,這時候使用 hidekeyboard 函數隱藏鍵盤
# python
driver.hide_keyboard()
函數封裝
在寫腳本的時候,把一些常用的功能合理封裝起來,能夠大大提高腳本執行的成功率。
- 滑動函數: 通過一組坐標點來進行滑動,從而實現一些曲線形的滑動。我們可以實現一個函數用來支持曲線滑動,輸入為形如[[x1, y1],[x2, y2]]的一組坐標點。
def swipe(points):
last_x = 0
last_y = 0
swipe_action = TouchAction(driver)
for i in range(0, len(points)):
x=points[i][0]
y=points[i][1]
if i == 0:
swipe_action = swipe_action.press(None, x, y).wait(20)
elif i == (len(points) - 1):
swipe_action = swipe_action.move_to(None, x - last_x, y - last_y).release()
swipe_action.perform()
else:
swipe_action = swipe_action.move_to(None, x - last_x, y - last_y).wait(20)
last_x = x
last_y = y
- 查找控件: 對於在調試的時候一切順利的腳本,真正到了雲測平台海量真機上測試的時候,卻經常出現控件找不到導致腳本執行失敗的問題。實際真機運行的時候,可能會有很多和本地並不相同的環境情況,比如網絡延遲導致控件較遲刷新出來,我們應當封裝一個較為穩定的控件查找函數。
appium本身有提供waitUntil的api,現在要找圖中的 個人中心 控件,使用顯示等待的方法如下:
from appium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait # available since 2.4.0
from selenium.webdriver.support import expected_conditions as EC
#通過xpath的方式搜索
element1 = WebDriverWait(driver, 3).until(EC.presence_of_element_located((By.XPATH,
"//android.widget.FrameLayout[1]/android.widget.LinearLayout[1]/android.widget.FrameLayout[1]/android.widget.LinearLayout[1]/android.widget.FrameLayout[1]/android.widget.LinearLayout[1]/android.widget.TabHost[1]/android.widget.LinearLayout[1]/android.widget.TabWidget[1]/android.view.View[4]")))
#通過resource-id的方式搜索,這裏底部導航的resource-id是相同,需要通過下標來區分,搜出多個elements後,需要指定需要的下標
element2 = WebDriverWait(driver, 3).until(EC.presence_of_all_elements_located((By.ID, "com.yunos.mqc:id/id_indicator_discovery")))
element2[3].click()
我們也可以封裝一個多方式定位控件的函數,這裏需要自己把握超時時間
import time
from time import sleep
def wait_for_element(xpath=None, id=None, text=None, index=None, timeout=3):
startTime = time.time()
nowTime = time.time()
while nowTime - startTime < timeout:
# 通過 xpath 查找控件
try:
if xpath is not None:
el = driver.find_element_by_xpath(xpath)
return el
except:
pass
# 通過 id 查找控件
try:
if id is not None:
if index is not None:
return driver.find_elements_by_id(self.id(id))[index]
else:
return driver.find_element_by_id(self.id(id))
except:
pass
# 通過 text 查找控件
try:
if text is not None:
return driver.find_element_by_name(text)
except:
pass
sleep(1)
nowTime = time.time()
raise Exception("Element id[%s] text[%s]" % (id, text))
組織用例
unittest是python的一個單元測試框架,它可以幫助我們有效組織用例,把用例的不同部分區分開來。結合已經封裝好的函數,我們寫一個登錄的測試腳本:
# -*- coding: UTF-8 -*-
import unittest
import time
import sys
from appium import webdriver
from time import sleep
from unittest import TestCase
from appium.webdriver.common.touch_action import TouchAction
from selenium.webdriver.common.touch_actions import TouchActions
class MqcAppium(TestCase):
#設備寬高
global width
global height
def get_desired_capabilities(self):
desired_caps = {
'platformName': 'Android',
'platformVersion': '18',
'deviceName': 'mqcDevice',
'udid': 'a05aacaf7d53',
'app': "D:\\appium\\test.apk",
'newCommandTimeout': 600,
'automationName': 'appium',
'unicodeKeyboard': True,
'resetKeyboard': True,
'appWaitActivity': 'com.yunos.mqc.view.activity.MainActivity',
}
return desired_caps
#unittest 啟動
def setUp(self):
desired_caps = self.get_desired_capabilities()
uri = "https://localhost:4723/wd/hub"
retry = 0
while retry < 2:
try:
self.driver = webdriver.Remote(uri, desired_caps)
break
except Exception, e:
retry += 1
if retry == 2:
raise e
sleep(10)
# 獲取當前設備分辨率
self.window_size = self.driver.get_window_size()
self.width = self.window_size["width"]
self.height = self.window_size["height"]
# unittest 用例,用 test_**** 命名
def test_login(self):
#大部分app啟動後會有動畫,啟動延遲等,視情況預留啟動延遲
sleep(5)
#通過 resource-id 與 index 查找 個人中心 控件
navPerson = self.wait_for_element(, index=3);
navPerson.click()
#通過 text 查找 尚未登錄
noLogin = self.wait_for_element(xpath=("//*[@text='%s']" % ("尚未登錄")));
noLogin.click()
#通過 xpath、resource-id 多種方式定位登錄控件,避免有些手機上 xpath 失效或者不一致的情況
inputUsername = self.wait_for_element(xpath="//android.widget.FrameLayout[1]/android.widget.LinearLayout[1]/android.widget.FrameLayout[1]/android.widget.LinearLayout[1]\
/android.widget.FrameLayout[1]/android.widget.LinearLayout[1]/android.widget.LinearLayout[1]/android.widget.EditText[1]", )
inputUsername.click()
inputUsername.send_keys("mqc_test")
inputPassword = self.wait_for_element(xpath="//android.widget.FrameLayout[1]/android.widget.LinearLayout[1]/android.widget.FrameLayout[1]/android.widget.LinearLayout[1]/\
android.widget.FrameLayout[1]/android.widget.LinearLayout[1]/android.widget.LinearLayout[1]/android.widget.EditText[2]", )
inputPassword.click()
inputPassword.send_keys("123456")
login = self.wait_for_element()
login.click()
#返回 廣場 並且向下滑動
navGround = self.wait_for_element(, index=0);
navGround.click()
#與前文 swipe 函數不同的是,為了兼容不同分辨率的手機,滑動操作應當使用 比例 而非 絕對坐標,當然,若是需要精確地滑動操作,建議使用scrollTo
self.swipe([[0.5, 0.7], [0.5, 0.6], [0.5, 0.5], [0.5, 0.4], [0.5, 0.3]])
def tearDown(self):
try:
self.driver.quit()
except:
pass
def swipe(self, points):
last_x = 0
last_y = 0
swipe_action = TouchAction(self.driver)
for i in range(0, len(points)):
x=float(points[i][0]) * self.width
y=float(points[i][1]) * self.height
if i == 0:
swipe_action = swipe_action.press(None, x, y).wait(20)
elif i == (len(points) - 1):
swipe_action = swipe_action.move_to(None, x - last_x, y - last_y).release()
swipe_action.perform()
else:
swipe_action = swipe_action.move_to(None, x - last_x, y - last_y).wait(20)
last_x = x
last_y = y
def wait_for_element(self, xpath=None, id=None, index=None, timeout=3):
startTime = time.time()
nowTime = time.time()
while nowTime - startTime < timeout:
# 通過 xpath 查找控件
try:
if xpath is not None:
el = self.driver.find_element_by_xpath(xpath)
return el
except:
pass
# 通過 id 查找控件
try:
if id is not None:
if index is not None:
return self.driver.find_elements_by_id(id)[index]
else:
return self.driver.find_element_by_id(id)
except:
pass
sleep(1)
nowTime = time.time()
raise Exception("Element xpath[%s] id[%s] index[%s] not found" % (xpath, id, index))
if __name__ == '__main__':
try: unittest.main()
except SystemExit: pass
最後更新:2017-09-11 16:32:32