UiAutomator2+Pytest+Allure+PO模型實現Android自動化測試


Uiautomator2介紹

uiautomator2 是一個可以使用Python對Android設備進行UI自動化的庫。其底層基於Google uiautomator,Google提供的uiautomator庫可以獲取屏幕上任意一個APP的任意一個控件屬性,並對其進行任意操作

環境搭建

  1. 安裝JDK,請參考此文章

  2. 安裝Android SDK,構建工具版本需大於24,下載並安裝工具包時請注意版本,SDK配置請參考此文章

  3. 安裝uiautomator2

    pip install uiautomator2	# 安裝uiautomator2
    uiautomator2 version		# 查看版本
    uiautomator2 --help			# 查看幫助
    
  4. 安裝查看器,用於元素定位輔助

    pip install weditor	# 安裝weditor
    weditor -v			# 查看版本
    weditor --help		# 查看幫助
    

    啟動查看器,在命令行輸入weditorpython -m weditor,又或者創建weditor桌面快捷鍵weditor --shortcut,通過桌面圖標運行程序

連接設備

首次連接設備會安裝【ATX】和【com.github.uiautomator.test】兩個軟件

  1. 手機開啟【開發者模式】,打開【USB調試模式】數據線連接手機,選擇【傳輸文件】

  2. 通過cmd進入命令行頁面,通過adb devices查看設備是否連接成功,adb相關命令請查看此文章

  3. 設備連接成功后打開查看器,在查看器頁面點擊【Connect】,出現綠色樹葉表示連接成功,左側會出現手機投屏(手機亮屏狀態)

  4. 通過Python腳本連接手機:

    import uiautomator2 as u2	# 導入uiautomator2庫並重命名為u2
    driver = u2.connect()		# 連接手機,若電腦只連接了一部手機,則不需要設備信息
    print(driver.info)			# 打印設備信息
    

    輸入如下系統信息,說明連接手機成功,就可以開始使用uiautomator2庫啦

    {'currentPackageName': 'com.oppo.launcher', 'displayHeight': 2297, 'displayRotation': 0, 'displaySizeDpX': 360, 'displaySizeDpY': 800, 'displayWidth': 1080, 'productName': 'OnePlus9R_CH', 'screenOn': True, 'sdkInt': 30, 'naturalOrientation': True}
    
    driver = u2.connect("48fd6742")	# 若電腦連接了多部手機,則需要添加設備序列號
    

常用操作

driver = u2.connect("48fd6742")	# 連接設備
driver.screen_on()  # 點亮屏幕
driver.screen_off() # 熄屏
driver.unlock() 	# 解鎖,真機測試發現密碼輸入頁面無法定位,部分APP也有無法同步登錄頁面導致不能定位的情況
print(driver.app_info("com.sankuai.meituan"))   # 獲取指定APP的信息
print(driver.app_list_running())# 列出所有正在運行的APP
print(driver.app_current()) 	# 獲取當前打開的APP信息
print(driver.window_size()) 	# 獲取屏幕大小
print(driver.device_info)   	# 獲取詳細的設備信息
print(driver.serial)	    	# 獲取設備序列號
print(driver.wlan_ip)   		# 獲取設備IP地址
driver.press("back")			# 點擊返回鍵
driver.open_notification()  	# 打開通知欄消息頁,關閉通知欄用滑屏操作
driver.open_quick_settings()    # 打開通知欄中的快速設置
driver.info.get("screen_on")	# 獲取當前屏幕是否為亮起狀態
driver.swipe(552,2066,552,700)  # 按絕對坐標滑動屏幕,默認滑動時長0.5s
driver.swipe_ext("up",scale=0.5)# 按方向滑動屏幕,設置滑動距離為屏幕寬度的50%,默認是90%
driver.open_url("https://www.baidu.com") # 直接調用默認瀏覽器並訪問指定網站
driver(description="信息").click()  		# 單擊短信APP
driver.double_click(0.375, 0.496)		 # 雙擊指定的相對坐標
driver(description="信息").long_click(1) 	# 長按短信APP,默認長按0.5s
driver(description="短信").send_keys("12")# 定位元素並輸入文本
driver(description="短信").set_text("A12")# 也是定位元素並輸入文本
driver(description="短信").clear_text()	# 清空輸入的信息
driver(resourceId="com.sankuai.meituan:id/passport_mobile_phone").get_text()	# 獲取文本內容
driver(text="美團").drag_to(0.375, 0.375,duration=1) # 將美團APP拖動到指定地點,持續時間1s,默認0.5s
driver.screenshot(r"D:\Download\test.png")			# 保存截屏到指定位置
driver.push(r"C:\Download\test.png","/sdcard/")		# 將截圖推送到手機中
driver.pull("/sdcard/rider.txt","rider.txt")		# 將手機中的文件傳送到電腦
driver.app_icon("com.sankuai.meituan").save(r"D:\Download\icon.png") # 獲取APP圖標並保存到指定位置
driver.app_start("com.sankuai.meituan",stop=True)	# 指定包名啟動APP,啟動前先結束應用運行狀態
driver.implicitly_wait(10)  # 原生操作,隱式等待,全局有效
driver(description="天天領紅包").exists()		   # 判斷元素是否出現,真返回True,否返回False
driver(description="電影/演出").wait(timeout=5)		# 等待元素出現,超時時間為5s
driver(description="跑腿").wait_gone(timeout=5)	 # 等待元素消失,超時時間為5s
driver.app_wait("com.sankuai.meituan",timeout=30,front=True)		# 等待程序開始前台運行,默認超時時間20s
driver.wait_activity("com.meituan.mmp.lib.mp.MPActivity0",timeout=5) # 等待活動頁加載完成,默認超時時間10s
driver.app_install(r"D:\Download\meituan.apk")	# 使用本地安裝包安裝
driver.app_install("http://www.meituan.com/mobile/download/meituan/android/meituan?from=new") # 在線下載安裝
driver.app_uninstall("com.sankuai.meituan")		# 卸載APP
driver.app_stop("com.sankuai.meituan")			# 關閉APP

元素定位

所選元素的屬性都可以用來定位,目的是通過這些屬性獲取唯一的定位元素,屬性可以組合定位,常用的定位如下

driver(text="運動健康")			# 通過文本定位
driver(description="我的")	 # 通過描述定位
driver.xpath('//*[@text="今日特價"]')				 # 通過Xpath定位
driver(className="android.widget.ImageView")		# 通過className定位
driver(resourceId="com.sankuai.meituan:id/button")	# 通過ID定位
driver(resourceId="com.android.systemui:id/tile_label", text="省電模式")	# 通過組合定位

還可以根據層次結構中的節點進行定位,如下

# 在android.widget.GridLayout的兄弟節點中找className=android.view.View的元素
driver(className="android.widget.GridLayout").sibling(className="android.view.View")
# 在android.widget.GridLayout的子節點中找第4個className=android.view.View的元素
driver(className="android.widget.GridLayout").child_by_instance(3,className="android.view.View")
# 在android.widget.LinearLayout的子節點中找className=android.widget.TextView且文本為“騎車”的元素,allow_scroll_search=True表示允許滑動屏幕進行查找
driver(className="android.widget.LinearLayout")\
    .child_by_text("騎車",allow_scroll_search=True,className="android.widget.TextView")
# 在android.widget.FrameLayout的子節點中找className=android.view.ViewGroup且描述為“評論插圖”的元素
driver(className="android.widget.FrameLayout")\
    .child_by_description("評論插圖",allow_scroll_search=True,className="android.view.ViewGroup")
# 通過方向(up/down/left/right)定位元素,如下:定位並點擊網易雲音樂APP右邊的APP
driver(text="網易雲音樂").right(className="android.widget.TextView").click()

斷言

通常通過判斷元素是否存在,或文本信息返回是否正確來確認響應結果,如下圖所示的兩種情況

使用assert判斷實際結果是否與預期結果一致

# 獲取登錄失敗的提示信息
text = driver(resourceId="com.sankuai.meituan:id/passport_account_tips").get_text()
# 判斷提示是否正確
assert text == "賬號或密碼錯誤,請重新輸入"
# 因登錄成功后才會出現成長值,所以通過查看成長值的元素是否存在來判斷是否登錄成功
assert driver(resourceId="com.sankuai.meituan:id/grouth_tv").exists

對於沒有焦點的,顯示時間有限的提示框,使用toast進行斷言

tips = driver.toast.get_message()	 # 獲取錯誤提示信息
assert "用戶名或密碼錯誤" in tips		# 判斷獲取的信息中是否有"用戶名或密碼錯誤"

案例演示

示例一:設置圖形驗證碼並解鎖,真機未能實現,無法進入解鎖界面,可能是權限問題,用模擬器執行成功

import time
import uiautomator2 as u2

driver = u2.connect()  # 連接設備
driver(text="設置").click()  # 點擊設置APP
# ↓使用節點方式定位元素
driver(className="android.widget.LinearLayout").child_by_text("安全", allow_scroll_search=True,className="android.widget.TextView").click()
driver(resourceId="android:id/title", text="屏幕鎖定").click()  # 使用ID方式定位元素
# ↓使用xpath方式定位元素
driver.xpath('//*[@resource-id="com.android.settings:id/list"]/android.widget.LinearLayout[3]/android.widget.RelativeLayout[1]').click()
driver.swipe_points([(0.223, 0.658), (0.5, 0.832), (0.5, 0.652), (0.78, 0.487)], 0.2)  # 多點滑動,設置圖形密碼
driver(resourceId="com.android.settings:id/footerRightButton").click()  # 點擊【繼續】
driver.swipe_points([(0.223, 0.658), (0.5, 0.832), (0.5, 0.652), (0.78, 0.487)], 0.2)
driver(resourceId="com.android.settings:id/footerRightButton").click()  # 點擊【確定】
driver(resourceId="com.android.settings:id/redaction_done_button").click()  # 點擊【完成】
driver.press("power")  # 點擊電源鍵
time.sleep(3)  # 操作太快只會熄滅點亮屏幕,所以等待3秒,讓設備上鎖
driver.unlock()  # 解鎖操作
driver.swipe_points([(0.273, 0.728), (0.5, 0.867), (0.5, 0.728), (0.719, 0.591)], 0.2)  # 繪制圖形密碼
assert driver(text="安全").exists # 判斷是否返回到安全頁面

示例二:以美團APP為例,測試登錄、查看訂單、搜索商品相關操作,因存在大量定位元素操作,不使用框架就不便於維護,所以使用Pytest+Allure及PO模型實現此操作,結構如下圖所示:

以登錄為例,首先創建基類basepage.py文件,封裝一些公共方法,比如:定位、點擊、輸入、清除、獲取文本信息、斷言等

import re

class BasePage():  # 構造函數
    def __init__(self, driver):
        self.driver = driver

    def click(self, element):  # 點擊
        if str(element).startswith("com"):  # 若開頭是com則使用ID定位
            self.driver(resourceId=element).click()  # 點擊定位元素
        elif re.findall("//", str(element)):  # 若//開頭則使用正則表達式匹配后用xpath定位
            self.driver.xpath(element).click()  # 點擊定位元素
        else:  # 若以上兩種情況都不是,則使用描述定位
            self.driver(description=element).click()  # 點擊定位元素

    def click_text(self, element):  # 點擊,根據文本定位
        self.driver(text=element).click()  # 點擊定位元素

    def clear(self, element):	# 清空輸入框中的內容
        if str(element).startswith("com"):  # 若開頭是com則使用ID定位
            self.driver(resourceId=element).clear_text()  # 清除文本
        elif re.findall("//", str(element)):  # 若//開頭則使用正則表達式匹配后用xpath定位
            self.driver.xpath(element).clear_text()  # 清除文本
        else:  # 若以上兩種情況都不是,則使用描述定位
            self.driver(description=element).clear_text()  # 清除文本

    def find_elements(self, element, timeout=5):  # 找元素
        is_exited = False
        try:
            while timeout > 0:
                xml = self.driver.dump_hierarchy()  # 獲取網頁層次結構
                if re.findall(element, xml):
                    is_exited = True
                    break
                else:
                    timeout -= 1
        except:
            print("元素未找到!")
        finally:
            return is_exited

    def assert_exited(self, element):  # 斷言元素是否存在
        assert self.find_elements(element) == True, "斷言失敗,{}元素不存在!".format(element)

然后創建封裝相應頁面操作的方法,比如登錄操作,創建login_page.py文件,此層主要是封裝操作流程

from base.basepage import BasePage	# 導入基類中封裝的方法
import allure	# 導入allure

class LoginPage(BasePage):
    # 元素定位,元素位置信息
    agreement = "com.sankuai.meituan:id/permission_agree_btn"
    get_loc_info = "com.android.permissioncontroller:id/permission_allow_foreground_only_button"
    get_pho_perm = "com.android.permissioncontroller:id/permission_deny_button"
    notice = "暫不"
    login_in_now = "com.sankuai.meituan:id/button"
    pwd_login = "com.sankuai.meituan:id/user_password_login"
    username = "com.sankuai.meituan:id/passport_mobile_phone"
    password = "com.sankuai.meituan:id/edit_password"
    tick = "com.sankuai.meituan:id/passport_account_checkbox"
    click_login = "com.sankuai.meituan:id/login_button"
    mine = "我的"
    growth = "com.sankuai.meituan:id/grouth_tv"

    # 行為,頁面操作
    def login(self,user,pwd):			# 登錄流程,以首次運行APP為例
        self.click(self.agreement)		# 同意使用APP協議
        self.click(self.get_loc_info)	# 獲取位置信息,選擇【使用時允許】
        self.click(self.get_pho_perm)	# 獲取電話權限,選擇【拒絕】
        self.click_text(self.notice)	# 是否開啟消息通知,選擇【暫不】
        self.click(self.login_in_now)	# 點擊首頁的【馬上登錄】
        self.click(self.pwd_login)		# 登錄方式選擇【密碼登錄】
        self.input(self.username,user)	# 輸入用戶名
        self.input(self.password,pwd)	# 輸入密碼
        self.click(self.tick)			# 勾選同意用戶協議
        self.click(self.click_login)	# 點擊【登錄】
        self.click(self.mine)			# 點擊【我的】
        self.assert_exited(self.growth) # 斷言,因登錄成功才顯示成長值,故以此元素是否出現判斷是否登錄成功
        self.click(self.homepage)		# 切到首頁

最后傳參並執行用例操作,因用Pytest框架,所以此文件注意格式,文件名需以test開頭,創建test_login.py文件

import uiautomator2 as u2
import pytest
from pageobject.login_page import LoginPage	# 導入上一步操作流程中的類

@allure.story('測試美團APP')
@allure.title("APP登錄")
# 連接手機並啟動APP進行登錄
class TestLogin:	# 類以Test開頭
    def test_login(self):   # 方法以test_或_test開頭
        driver = u2.connect("48fd6742")
        driver.app_start("com.sankuai.meituan", stop=True)
        login_page = LoginPage(driver=driver)
        login_page.login("16666666666","123456abc")

至此登錄操作就算完成,執行測試就可以啦!

搜索操作也是相同的操作,把公共方法直接寫入到基類文件中,操作流程單獨創建search_page.py文件

from base.basepage import BasePage	# 導入基類,直接調用公共方法
import allure

class SearchPage(BasePage):
    # 元素定位信息
    inputfield = "com.sankuai.meituan:id/search_edit_flipper_container"
    clicksearch = "com.sankuai.meituan:id/search"
    inputvalue = "com.sankuai.meituan:id/search_edit"
    back = "com.sankuai.meituan:id/back"

    def search(self,value):					# 搜索操作流程
        self.click(self.inputfield)			# 點擊搜索框
        self.clear(self.inputvalue)			# 每次搜索前線清空搜索框
        self.input(self.inputvalue,value)	# 定位輸入框並輸入關鍵字
        self.click(self.clicksearch)		# 點擊【搜索】按鈕
        self.click(self.back)				# 返回到上一級
        self.click(self.back)				# 返回到首頁

創建傳參及執行搜索操作的文件test_search.py

import uiautomator2 as u2
import pytest
from pageobject.search_page import SearchPage	# 導入上一步操作流程中的類

class TestSearch:
    keyword = ["奶茶","桌球","景點"]
    @pytest.mark.parametrize("value",keyword)	# 使用pytest參數化,搜索三個關鍵字
    @allure.title("搜索")
    def test_search(self,value):	
        driver = u2.connect("48fd6742")
        driver.app_start("com.sankuai.meituan")
        search_page = SearchPage(driver=driver)
        search_page.search(value)

使用allure執行測試並生成報告

pytest --alluredir=./result testcases # 執行testcases文件下所有測試用例,並將中間結果生成到result目錄中
allure serve ./result	# 生成最終報告

關於Pytest、Allure的使用請查看此文章

關於PO模型請查看此文章


免責聲明!

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



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