基於UiAutomator2+PageObject模式開展APP自動化測試實戰

2022-08-10 12:01:55

前言

在上一篇《APP自動化測試框架-UiAutomator2基礎》中,重點介紹了uiautomator2的專案組成、執行原理、環境搭建及元素定位等基礎入門知識,本篇將介紹如何基於uiautomator2設計PageObject模式(以下簡稱PO模式)、開展移動APP的自動化測試實踐。

一、PO模式簡介

1.起源

PO模式是國外大神Martin Fowler於2013年提出來的一種設計模式,其基本思想是強調程式碼邏輯和業務邏輯相分離。https://martinfowler.com/bliki/PageObject.html

 

 

2.PO六大原則

翻譯成中文就是:

  • 公共方法表示頁面提供的服務
  • 儘量不要暴露頁面的內部實現
  • 頁面中不要加斷言,斷言載入
  • 方法返回另外的頁面物件
  • 不需要封裝全部的頁面元素
  • 相同的行為、不同的結果,需要封裝成不同的方法

3.PO設計模式分析

  1. 用Page Object表示UI
  2. 減少重複樣本程式碼
  3. 讓變更範圍控制在Page Object內
  4. 本質是物件導向程式設計

4.PO封裝的主要組成元素

  • Driver物件:完成對WEB、Android、iOS、介面的驅動
  • Page物件:完成對頁面的封裝
  • 測試用例:呼叫Page物件實現業務並斷言
  • 資料封裝:組態檔和資料驅動
  • Utils:其他功能/工具封裝,改善原生框架不足

5.業內常見的分層模型

 

1)四層模型

  • Driver層完成對webdriver常用方法的二次封裝,如:定位元素方法;
  • Elements層:存放元素屬性值,如圖示、按鈕的resourceId、className等;
  • Page層:存放頁面物件,通常一個UI介面封裝一個物件類;
  • Case層:呼叫各個頁面物件類,組合業務邏輯、形成測試用例;

2)三層模型(推薦)

四層模型與三層模型唯一的區別就是將Page層與Elements層存放在一起,各個頁面物件檔案同時包含當前頁面中各個圖示、按鈕的resourceId、className等屬性值,以便隨時呼叫;

二、GUI自動化測試二三事

1.什麼是自動化

自動化顧名思義就是把人對軟體的操作行為通過程式碼或工具轉換為機器執行測試的過程或實踐。

2.為什麼要做自動化

這個可說的內容就太多了,不做過多贅述,詳情可參照我整理的《軟體測試52講》課堂筆記中的內容:

3.什麼樣的專案適合做自動化

  • 需求穩定,不會頻繁變更(尤其是GUI測試,頁面佈局及元素不能頻繁變化)
  • 研發和維護週期長,需要頻繁執行迴歸測試
  • 手工測試無法實現或成本高,需要用自動化代替實現
  • 需要重複執行的測試場景
  • ......

三、APP自動化測試實戰

1.設計專案結構

2.封裝BasePage

即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)

 

3.定義各個頁面Page

所有頁面Page類都繼承BasePage。根據PO模式六大原則之一的

  • home_page.py
  • chat_page.py
  • group_page.py

1)home_page.py

# 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()

 

2)chat_page.py

# 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()

 

3)group_page.py

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()

 

4.編寫測試用例

測試用例實際上是呼叫各個頁面物件組合成的一個業務邏輯集合,中間再加入一些控制結構(選擇結構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")

 

5.執行效果

 

小結

以上就是利用uiautomator2結合PO模式測試行動端APP的一次實踐,介紹了:

  • PO模式相關概念:六大原則、設計模式、PO封裝元素組成、業內常見的分層模型
  • GUI自動化測試:為什麼要做自動化即自動化的利弊、什麼樣的專案適合做自動化
  • APP自動化測試實踐:如何設計專案結構、封裝頁面基礎類別、定義頁面物件、編寫測試用例

當然,你還可以藉助業內常見的一些PO庫,如page_objects,從而更加簡便地設計測試框架、組織用例等,但核心思想一直不變,都是為了實現程式碼邏輯和業務邏輯分離,從而達到靈活複用、以不變應萬變的目的。

 

更多實戰乾貨,歡迎掃碼關注!