在上一篇《APP自動化測試框架-UiAutomator2基礎》中,重點介紹了uiautomator2的專案組成、執行原理、環境搭建及元素定位等基礎入門知識,本篇將介紹如何基於uiautomator2設計PageObject模式(以下簡稱PO模式)、開展移動APP的自動化測試實踐。
PO模式是國外大神Martin Fowler於2013年提出來的一種設計模式,其基本思想是強調程式碼邏輯和業務邏輯相分離。https://martinfowler.com/bliki/PageObject.html
翻譯成中文就是:
四層模型與三層模型唯一的區別就是將Page層與Elements層存放在一起,各個頁面物件檔案同時包含當前頁面中各個圖示、按鈕的resourceId、className等屬性值,以便隨時呼叫;
自動化顧名思義就是把人對軟體的操作行為通過程式碼或工具轉換為機器執行測試的過程或實踐。
這個可說的內容就太多了,不做過多贅述,詳情可參照我整理的《軟體測試52講》課堂筆記中的內容:
即Driver層,對uiautomator2進行二次封裝,所有Page類都會直接或間接繼承BasePage
# coding:utf-8 DEFAULT_SECONDS = 10 class BasePage(object): """ 第一層:對uiAutomator2進行二次封裝,定義一個所有頁面都繼承的BasePage 封裝uiAutomator2基本方法,如:元素定位,元素等待,導航頁面等 不需要全部封裝,用到多少就封裝多少 """ def __init__(self, device): self.d = device def by_id(self, id_name): """通過id定位單個元素""" try: self.d.implicitly_wait(DEFAULT_SECONDS) return self.d(resourceId=id_name) except Exception as e: print("頁面中沒有找到id為%s的元素" % id_name) raise e def by_id_matches(self, id_name): """通過id關鍵字匹配定位單個元素""" try: self.d.implicitly_wait(DEFAULT_SECONDS) return self.d(resourceIdMatches=id_name) except Exception as e: print("頁面中沒有找到id為%s的元素" % id_name) raise e def by_class(self, class_name): """通過class定位單個元素""" try: self.d.implicitly_wait(DEFAULT_SECONDS) return self.d(className=class_name) except Exception as e: print("頁面中沒有找到class為%s的元素" % class_name) raise e def by_text(self, text_name): """通過text定位單個元素""" try: self.d.implicitly_wait(DEFAULT_SECONDS) return self.d(text=text_name) except Exception as e: print("頁面中沒有找到text為%s的元素" % text_name) raise e def by_class_text(self, class_name, text_name): """通過text和class多重定位某個元素""" try: self.d.implicitly_wait(DEFAULT_SECONDS) return self.d(className=class_name, text=text_name) except Exception as e: print("頁面中沒有找到class為%s、text為%s的元素" % (class_name, text_name)) raise e def by_text_match(self, text_match): """通過textMatches關鍵字匹配定位單個元素""" try: self.d.implicitly_wait(DEFAULT_SECONDS) return self.d(textMatches=text_match) except Exception as e: print("頁面中沒有找到text為%s的元素" % text_match) raise e def by_desc(self, desc_name): """通過description定位單個元素""" try: self.d.implicitly_wait(DEFAULT_SECONDS) return self.d(description=desc_name) except Exception as e: print("頁面中沒有找到desc為%s的元素" % desc_name) raise e def by_xpath(self, xpath): """通過xpath定位單個元素【特別注意:只能用d.xpath,千萬不能用d(xpath)】""" try: self.d.implicitly_wait(DEFAULT_SECONDS) return self.d.xpath(xpath) except Exception as e: print("頁面中沒有找到xpath為%s的元素" % xpath) raise e def by_id_text(self, id_name, text_name): """通過id和text多重定位""" try: self.d.implicitly_wait(DEFAULT_SECONDS) return self.d(resourceId=id_name, text=text_name) except Exception as e: print("頁面中沒有找到resourceId、text為%s、%s的元素" % (id_name, text_name)) raise e def find_child_by_id_class(self, id_name, class_name): """通過id和class定位一組元素,並查詢子元素""" try: self.d.implicitly_wait(DEFAULT_SECONDS) return self.d(resourceId=id_name).child(className=class_name) except Exception as e: print("頁面中沒有找到resourceId為%s、className為%s的元素" % (id_name, class_name)) raise e def is_text_loc(self, text): """定位某個文字物件(多用於判斷某個文字是否存在)""" return self.by_text(text_name=text) def is_id_loc(self, id): """定位某個id物件(多用於判斷某個id是否存在)""" return self.by_id(id_name=id) def fling_forward(self): """當前頁面向上滑動""" return self.d(scrollable=True).fling.vert.forward() def swipe_up(self): """當前頁面向上滑動,步長為10""" return self.d(scrollable=True).swipe("up", steps=10) def swipe_down(self): """當前頁面向下滑動,步長為10""" return self.d(scrollable=True).swipe("down", steps=10) def swipe_left(self): """當前頁面向左滑動,步長為10""" return self.d(scrollable=True).swipe("left", steps=10) def swipe_right(self): """當前頁面向右滑動,步長為10""" return self.d(scrollable=True).swipe("right", steps=10)
所有頁面Page類都繼承BasePage。根據PO模式六大原則之一的
# coding:utf-8 from pages.u2_base_page import BasePage class HomePage(BasePage): def __init__(self, device): super(YueYunHome, self).__init__(device) self.msg_icon = "com.zhoulesin.imuikit2:id/icon_msg" self.friend_icon = "com.zhoulesin.imuikit2:id/icon_friend" self.find_icon = "com.zhoulesin.imuikit2:id/icon_find" self.mine_icon = "com.zhoulesin.imuikit2:id/icon_mine" self.add_icon = "com.zhoulesin.imuikit2:id/iv_chat_add" self.create_group_btn = "com.zhoulesin.imuikit2:id/ll_create_group" self.chat_list = "com.zhoulesin.imuikit2:id/rv_message_list" self.chat_list_child = "com.zhoulesin.imuikit2:id/ll_content" def msg_icon_obj(self): """對談圖示""" return self.by_id(id_name=self.msg_icon) def click_msg_icon(self): """點選底部對談圖示""" return self.by_id(id_name=self.msg_icon).click() def click_friend_icon(self): """點選底部通訊錄圖示""" return self.by_id(id_name=self.friend_icon).click() def click_find_icon(self): """點選底部發現圖示""" return self.by_id(id_name=self.find_icon).click() def click_mine_icon(self): """點選底部我的圖示""" return self.by_id(id_name=self.mine_icon).click() def click_add_icon(self): """點選右上角+號圖示""" return self.by_id(id_name=self.add_icon).click() def click_create_group_btn(self): """點選右上角+號圖示""" return self.by_id(id_name=self.create_group_btn).click()
# coding:utf-8 from pages.u2_base_page import BasePage class ChatPage(BasePage): def __init__(self, device): super(SingleChat, self).__init__(device) self.msg_icon = "com.zhoulesin.imuikit2:id/icon_msg" self.friend_icon = "com.zhoulesin.imuikit2:id/icon_friend" self.find_icon = "com.zhoulesin.imuikit2:id/icon_find" self.mine_icon = "com.zhoulesin.imuikit2:id/icon_mine" self.content = "com.zhoulesin.imuikit2:id/et_content" self.send_button = "com.zhoulesin.imuikit2:id/btn_send" self.more_button = "com.zhoulesin.imuikit2:id/btn_more" self.album_icon = "com.zhoulesin.imuikit2:id/photo_layout" self.finish_button = "com.zhoulesin.imuikit2:id/btn_ok" def open_chat_by_name(self, name): """根據對談名開啟對談""" return self.by_text(text_name=name).click() def send_text(self, text): """傳送文字訊息""" return self.by_id(id_name=self.content).send_keys(text) def click_send_button(self): """點選傳送按鈕""" return self.by_id(id_name=self.send_button).click() def click_bottom_side(self): """點選對談介面底部區域、喚起鍵盤""" return self.d.click(0.276, 0.973) def click_more_button(self): """點選+號按鈕""" return self.by_id(id_name=self.more_button).click() def album_icon_obj(self): """相簿圖示""" return self.by_id(id_name=self.album_icon) def click_album_icon(self): """點選相簿圖示開啟相簿""" return self.by_id(id_name=self.album_icon).click() def select_picture(self, range_int): """點選相簿中的圖片選擇圖片""" return self.by_xpath( '//*[@resource-id="com.zhoulesin.imuikit2:id/recycler"]/android.widget.FrameLayout[%d]' % range_int).click() def click_finish_button(self): """點選完成按鈕、傳送圖片""" return self.by_id(id_name=self.finish_button).click()
from pages.u2_base_page import BasePage class GroupPage(BasePage): def __init__(self, device): super().__init__(device) self.friend_list = "com.zhoulesin.imuikit2:id/rv_friend_list" self.friend_list_child = "com.zhoulesin.imuikit2:id/iv_select" self.confirm_btn = "com.zhoulesin.imuikit2:id/tv_confirm" self.more_icon = "com.zhoulesin.imuikit2:id/img_right" self.group_name = "群聊名稱" self.group_name_edit_context = "com.zhoulesin.imuikit2:id/et_group_name" self.finish_btn = "com.zhoulesin.imuikit2:id/tv_btn" self.group_icon = "com.zhoulesin.imuikit2:id/ll_my_group" self.group_list = "com.zhoulesin.imuikit2:id/rv_group_list" self.group_list_child = "com.zhoulesin.imuikit2:id/name" def select_group_member(self): """選擇群成員,全部選擇""" friend_list = self.by_id(self.friend_list).child(resourceId=self.friend_list_child) for i in range(len(friend_list)): friend_list[i].click() def click_confirm_btn(self): """點選確認按鈕""" return self.by_id(id_name=self.confirm_btn).click() def click_more_icon(self): """點選群聊設定中右上角的更多圖示""" return self.by_id(id_name=self.more_icon).click() def modify_group_name(self, group_name): """點選群聊設定中右上角的更多圖示""" self.by_text(self.group_name).click() self.by_id(self.group_name_edit_context).send_keys(group_name) self.by_id(self.finish_btn).click() def click_group_icon(self): """點選群組圖示,進入群組列表""" return self.by_id(self.group_icon).click()
測試用例實際上是呼叫各個頁面物件組合成的一個業務邏輯集合,中間再加入一些控制結構(選擇結構if...else、迴圈結構for)、斷言等,就形成了最終的測試用例。
# coding:utf-8 import random import uiautomator2 as u2 from pages.home_page import HomePage from pages.chat_page import ChatPage class TestYueYun: def setup(self): device = 'tkqkssgirgaipblj' # 裝置序列號 apk = 'com.zhoulesin.imuikit2' # 包名 self.d = u2.connect(device) self.d.app_start(apk) self.home = HomePage(self.d) self.chat = ChatPage(self.d) def test_send_msg(self): """測試傳送文字訊息""" self.home.click_msg_icon() # 點選底部訊息圖示,進入主頁 self.chat.open_chat_by_name("張三") # 點開名為「張三」的聯絡人對談 self.chat.click_bottom_side() # 點選底部區域,喚起鍵盤 self.chat.send_text("開始傳送訊息...") # 輸入框輸入文字 self.chat.click_send_button() # 點選傳送按鈕 for i in range(1, 10): # 傳送10條訊息:1-10,範圍及傳送的內容也可以自定義 self.chat.send_text(i) self.chat.click_send_button() self.chat.send_text("測試完成!") self.chat.click_send_button() # 返回主頁 while not self.home.msg_icon_obj().exists(): self.d.press("back") def test_send_picture(self): """測試傳送圖片""" self.home.click_msg_icon() # 點選底部訊息圖示,進入主頁 self.chat.open_chat_by_name("群聊一") # 點開名為「群聊一」的對談 self.chat.click_bottom_side() # 點選底部區域,喚起鍵盤 self.chat.send_text("測試傳送圖片...") # 輸入框輸入文字 self.chat.click_send_button() # 點選傳送(+)號按鈕,彈出相簿選項 for i in range(2): # 傳送圖示的次數 # 判斷當相簿圖示不存在時,點選(+)號從鍵盤模式切換為選擇圖片視訊等 if not self.chat.album_icon_obj().exists(): self.chat.click_more_button() self.chat.click_album_icon() # 點選相簿圖示,進入相簿選擇圖片 for a in range(3): # 一次性選擇3張圖片 # 從相簿child子列表中指定範圍內隨機選擇3張圖片 self.chat.select_picture(range_int=random.randint(1, 20)) self.chat.click_finish_button() # 點選傳送按鈕,傳送圖片 if not self.chat.album_icon_obj().exists(): self.chat.click_more_button() self.chat.send_text("測試完成!") self.chat.click_send_button() # 返回主頁 while not self.home.msg_icon_obj().exists(): self.d.press("back")
以上就是利用uiautomator2結合PO模式測試行動端APP的一次實踐,介紹了:
當然,你還可以藉助業內常見的一些PO庫,如page_objects,從而更加簡便地設計測試框架、組織用例等,但核心思想一直不變,都是為了實現程式碼邏輯和業務邏輯分離,從而達到靈活複用、以不變應萬變的目的。
更多實戰乾貨,歡迎掃碼關注!