天天看點

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

前言

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

一、PO模式簡介

1.起源

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

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

2.PO六大原則

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

翻譯成中文就是:

  • 公共方法表示頁面提供的服務
  • 盡量不要暴露頁面的内部實作
  • 頁面中不要加斷言,斷言加載
  • 方法傳回另外的頁面對象
  • 不需要封裝全部的頁面元素
  • 相同的行為、不同的結果,需要封裝成不同的方法

3.PO設計模式分析

  1. 用Page Object表示UI
  2. 減少重複樣本代碼
  3. 讓變更範圍控制在Page Object内
  4. 本質是面向對象程式設計

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

  • Driver對象:完成對WEB、Android、iOS、接口的驅動
  • Page對象:完成對頁面的封裝
  • 測試用例:調用Page對象實作業務并斷言
  • 資料封裝:配置檔案和資料驅動
  • Utils:其他功能/工具封裝,改善原生架構不足

5.業内常見的分層模型

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

1)四層模型

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

2)三層模型(推薦)

四層模型與三層模型唯一的差別就是将Page層與Elements層存放在一起,各個頁面對象檔案同時包含目前頁面中各個圖示、按鈕的resourceId、className等屬性值,以便随時調用;

二、GUI自動化測試二三事

1.什麼是自動化

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

2.為什麼要做自動化

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

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

3.什麼樣的項目适合做自動化

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

三、APP自動化測試實戰

1.設計項目結構

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

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模式六大原則之一的“不需要封裝全部的頁面元素”,用到多少頁面元素就封裝多少。例如:目前待測APP有3個界面,則定義3個頁面Page:

  • 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+PageObject模式開展APP自動化測試實戰

小結

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

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

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

繼續閱讀