微信小程序UI自動(dòng)化測(cè)試實(shí)踐:Minium+PageObject

小程序架構(gòu)上分為渲染層和邏輯層,盡管各平臺(tái)的運(yùn)行環(huán)境十分相似,但是還是有些許的區(qū)別(如下圖),比如說(shuō)JavaScript 語(yǔ)法和 API 支持不一致,WXSS 渲染表現(xiàn)也有不同,所以不論是手工測(cè)試,還是UI自動(dòng)化測(cè)試,都必須要在 iOS 和 Android 上分別檢查小程序的真實(shí)表現(xiàn)。

由于生態(tài)方面的原因,目前可選擇的小程序UI自動(dòng)化框架較少。在框架選取過(guò)程中,我考察了Appium、Airtest和Minium三個(gè)框架,并將三者做了對(duì)比,形成了以下圖表:

Appium實(shí)現(xiàn)微信小程序自動(dòng)化測(cè)試的手段基本上還是套用針對(duì) Hybrid App 的測(cè)試方案,通過(guò)定位H5 App資源控件,并結(jié)合屏幕坐標(biāo)的方式來(lái)操控小程序的頁(yè)面元素;網(wǎng)易推出的Airtest則是基于圖像識(shí)別和Poco控件識(shí)別,之前也對(duì)此框架做過(guò)比較深入的了解,但是和Appium一樣,對(duì)于小程序自動(dòng)化測(cè)試來(lái)說(shuō),以上兩者無(wú)法深入小程序邏輯層,只能作用于渲染層,從另外一個(gè)角度來(lái)說(shuō),這兩個(gè)框架還屬于黑盒自動(dòng)化測(cè)試的范疇。
01 Minium
接下來(lái)再介紹一下今天的主角:Minium。它是微信小程序官方推出自動(dòng)化框架,提供了 Python3 和 JavaScript 版本(后者目前已停止維護(hù),后文中的minium單指Python版本),目前最新的版本為1.0.0b2。minium不僅限于 UI 自動(dòng)化,它還提供了很多有用的特性,比如說(shuō)支持調(diào)用和 Mock 部分 wx 對(duì)象上的接口,支持獲取和設(shè)置小程序頁(yè)面數(shù)據(jù),支持直接觸發(fā)小程序元素綁定事件等等。
另外,minium提供一個(gè)基于unittest封裝好的測(cè)試框架,利用這個(gè)簡(jiǎn)單的框架對(duì)小程序測(cè)試也可以起到事半功倍的效果。有了以上功能,不但可以簡(jiǎn)化用例的一些前期準(zhǔn)備工作,更可以對(duì)小程序做更針對(duì)和更全面的測(cè)試。
minium的下載安裝和官方文檔,可以在代碼庫(kù)查看。官方文檔寫(xiě)的還算較為清晰,除此之外,以下網(wǎng)站在學(xué)習(xí)過(guò)程中也有幫助:
微信開(kāi)放社區(qū): 一些minium使用方面的問(wèn)題,可以在右上角搜索 "minium" 尋找答案或發(fā)起提問(wèn);
微信開(kāi)發(fā)者工具: minium與微信開(kāi)發(fā)者工具強(qiáng)關(guān)聯(lián),開(kāi)發(fā)調(diào)試腳本都需要使用微信開(kāi)發(fā)者工具;
Minium Demo: 官方提供的python版本的demo,內(nèi)容非常簡(jiǎn)單,可以用來(lái)簡(jiǎn)單熟悉一下框架,若要運(yùn)行demo需要先下載示例小程序代碼;
02 Minium + Page Object
早期 GUI 自動(dòng)化測(cè)試腳本,無(wú)論是Selenium還是UFT,腳本通常是由一系列的頁(yè)面控件的順序操作組成的,有點(diǎn)像操作級(jí)別的“流水賬”,這主要體現(xiàn)在以下幾個(gè)方面:
腳本邏輯層次不夠清晰,屬于 All-in-one 的風(fēng)格,既有頁(yè)面元素的定位查找,又有對(duì)元素的操作;
腳本的可讀性差,在實(shí)際項(xiàng)目中,很難從代碼中直觀看出到底腳本在操作哪個(gè)控件,并且腳本的每一行都直接描述各個(gè)頁(yè)面上的元素操作,無(wú)法直觀的看出腳本更高層的業(yè)務(wù)測(cè)試流程;
通用步驟會(huì)在大量測(cè)試腳本中重復(fù)出現(xiàn);
Page Object 就是為了解決以上問(wèn)題而出現(xiàn)的,它是UI自動(dòng)化測(cè)試項(xiàng)目開(kāi)發(fā)實(shí)踐的最佳設(shè)計(jì)模式,采用分層封裝的設(shè)計(jì)思想,不同層關(guān)心不同的問(wèn)題。頁(yè)面對(duì)象層只關(guān)心元素定位問(wèn)題,測(cè)試用例只關(guān)心測(cè)試的數(shù)據(jù)。通過(guò)對(duì)界面元素和功能模塊的封裝減少冗余代碼,在后期維護(hù)中,若元素定位或功能模塊發(fā)生變化,只需要調(diào)整頁(yè)面元素或功能模塊封裝的代碼,顯著提高測(cè)試用例的可維護(hù)性。
基于PO模式,小程序UI自動(dòng)化測(cè)試Demo項(xiàng)目的目錄結(jié)構(gòu)及說(shuō)明如下:

cases/: 存放業(yè)務(wù)測(cè)試用例;
outputs/: Minium測(cè)試報(bào)告;
pages/:頁(yè)面對(duì)象模型;
*config.json:Minium項(xiàng)目配置文件;
suite.json:Minium測(cè)試計(jì)劃文件;
route.py:統(tǒng)一存放小程序頁(yè)面路由;
utils.py:存放一些公共方法;
03 具體代碼
下面從具體代碼入手,簡(jiǎn)單講述一下項(xiàng)目的設(shè)計(jì)思路。
首先是BasePage,它是頁(yè)面模型基類(lèi),用于封裝所有頁(yè)面公用的方法。
import abc
import time
class BasePage(abc.ABC):
? ? def __init__(self, mini, route, title=""):
? ? ? ? ?self.mini = mini
? ? ? ? ?self.route = route
? ? ? ? ?self.title = title?
? def open(self):
? ? ? ?"""跳轉(zhuǎn)到小程序目標(biāo)頁(yè)面"""
? ? ? ? self._open(self.route, self.title, open_type='redirect')
? ? def check_element(self):
? ? ? ? """頁(yè)面元素審查
? ? ? ? 在子類(lèi)中實(shí)現(xiàn)此方法時(shí),建議使用Minium框架中提供的斷言方法,原因如下:
? ? ? ? 調(diào)用 Minium 框架提供的斷言方法,會(huì)攔截 assert 調(diào)用,記錄運(yùn)行時(shí)數(shù)據(jù)和截圖,自動(dòng)在測(cè)試報(bào)告
? ? ? ? 中生成截圖 (需要在配置文件中將 assert_capture 設(shè)置為T(mén)rue)
? ? ? ? 但是如果直接assert或使用unittest.TestCase提供的斷言,當(dāng)斷言失敗時(shí),無(wú)法自動(dòng)生成截圖
? ? ? ? """
? ? ? ? raise NotImplementedError? ? def on_page(self, route,title=None, wait_util_page_contain_keys: list = None):
? ? ? ? """通過(guò)對(duì)title和route斷言,校驗(yàn)跳轉(zhuǎn)進(jìn)入的當(dāng)前頁(yè)是否符合預(yù)期"""
? ? ? ? if wait_util_page_contain_keysis not None and isinstance(wait_util_page_contain_keys, list):
? ? ? ? ? ? self.mini.page.wait_data_contains(wait_util_page_contain_keys)
? ? ? ? else:
? ? ? ? ? ? time.sleep(2)
? ? ? ? self.mini.assertEqual(self.current_route, route,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? msg="頁(yè)面路由不匹配, 預(yù)期:{},實(shí)際:{}".format(route, self.current_route))
? ? ? ? if title:
? ? ? ? ? ? self.mini.assertEqual(self.current_title, title,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? msg="頁(yè)面標(biāo)題不匹配, 預(yù)期:{},實(shí)際:{}".format(title, self.current_title))
? ? ?@property
? ? ?def current_title(self) -> str:
? ? ? ? """獲取當(dāng)前頁(yè)面 head title, 具體項(xiàng)目具體分析,以下代碼僅用于演示"""
? ? ? ? return self.mini.page.get_element("XXXXXX").inner_text
? ? ?@property
? ? ?def current_route(self) -> str:
? ? ? ? """獲取當(dāng)前頁(yè)面route, 具體項(xiàng)目具體分析, 以下代碼僅用于演示"""
? ? ? ? return self.mini.app.get_current_page().path? ? ?
def _open(self, route, title, open_type=None):
? ? ? ? """
? ? ? ? 小程序頁(yè)面跳轉(zhuǎn)可以使用以下三個(gè)方法, 一些區(qū)別如下:
? ? ? ? 1.`navigate_to`: 此方法會(huì)保留當(dāng)前頁(yè)面,并跳轉(zhuǎn)到應(yīng)用內(nèi)的某個(gè)頁(yè)面(不能跳到tabbar頁(yè)面). 小程序中頁(yè)面棧最多十層, 如果超過(guò)十層時(shí),再使用此方法
? ? ? ? 跳轉(zhuǎn)頁(yè)面, 會(huì)拋出以下異常:`minium.framework.exception.MiniAppError: webview count limit exceed`. 因此需要在運(yùn)行用例后及時(shí)清除頁(yè)面棧;
? ? ? ? 2. `redirect_to`: 關(guān)閉當(dāng)前頁(yè)面,重定向到應(yīng)用內(nèi)的某個(gè)頁(yè)面,使用此方法跳轉(zhuǎn)頁(yè)面時(shí),會(huì)替換頁(yè)面棧,因此頁(yè)面棧不會(huì)超限,但是也導(dǎo)致不支持頁(yè)面回退;
? ? ? ? 3. `relaunch`: 關(guān)閉所有頁(yè)面,清空頁(yè)面棧,打開(kāi)到應(yīng)用內(nèi)的某個(gè)頁(yè)面;
? ? ? ? """
? ? ? ? open_type = 'redirect' if open_type is None else open_type? ? ? ?if open_type.lower() == "navigate":
? ? ? ? ? ? self.mini.app.navigate_to(route)
? ? ? ? elif open_type.lower() == "redirect":
? ? ? ? ? ? self.mini.app.redirect_to(route)
? ? ? ? else:
? ? ? ? ? ? self.mini.app.relaunch(route)
? ? ? ? self.on_page(route, title)
具體業(yè)務(wù)的頁(yè)面模型對(duì)象都需要繼承BasePage,以IndexPage為例,代碼如下所示:
from pages.BasePage import BasePage
from route import XXXXX
?
class IndexPage(BasePage):
? ? locators = {
? ? ? ? "AAA": "view#aaa",??
? ? ? ? "BBB": "view.bbb>image"
? ? }
? ? def check_element(self):
? ? ? ? self.mini.assertTrue(self.mini.page.element_is_exists(IndexPage.locators['AAA']) is True)
? ? ? ? self.mini.assertTrue(self.mini.page.element_is_exists(IndexPage.locators['BBB']) is True)
? ? def click_query_btn(self):
? ? ? ? self.mini.page.get_element("view", inner_text="xxxx").click()
? ? ? ? self.on_page(route=XXXXX.XXXX.route, title=XXXXX.XXXX.title)
BaseEntity為測(cè)試用例基類(lèi),用于統(tǒng)一設(shè)置用例準(zhǔn)備和清理工作,所有項(xiàng)目的測(cè)試用例都繼承此類(lèi):
from pathlib import Path
import minium
?
class BaseEntity(minium.MiniTest):
? ? """測(cè)試用例基類(lèi)"""
? ? @classmethod
? ? def setUpClass(cls):
? ? ? ? ?super(BaseEntity, cls).setUpClass()
? ? ? ? ?output_dir = Path(cls.CONFIG.outputs)
? ? ? ? ?if not output_dir.is_dir():
? ? ? ? ? ? ?output_dir.mkdir()
? ? @classmethod
? ? def tearDownClass(cls):
? ? ? ? super(BaseEntity, cls).tearDownClass()
? ? ? ? cls.app.go_home()
? ? def setUp(self):
? ? ? ? super(BaseEntity, self).setUp()
? ? def tearDown(self):
? ? ? ? super(BaseEntity, self).tearDown()
cases.Moudle_1.index_test.IndexTest代碼內(nèi)容如下:
from cases import BaseEntity
from pages.Moudle_1.IndexPage import IndexPage
from route import XXXXX
?
class ParkIndexTest(BaseEntity):
? ? def test_index_page(self):
? ? ? ? ?index_page = IndexPage(self, XXXXX.INDEX.route, XXXXX.INDEX.title)
? ? ? ? ?index_page.open()
? ? ? ? ?index_page.check_element()
? ? ? ? ?index_page.click_query_btn()
總結(jié):
優(yōu)點(diǎn):PO模式對(duì)頁(yè)面界面交互細(xì)節(jié)進(jìn)行了封裝,而測(cè)試用例基于頁(yè)面對(duì)象完成具體操作,這樣可以使我們的自動(dòng)化測(cè)試腳本案例更關(guān)注業(yè)務(wù),而非界面細(xì)節(jié),提高了測(cè)試案例的可讀性。
缺點(diǎn)(個(gè)人觀點(diǎn)):開(kāi)發(fā)和維護(hù)頁(yè)面對(duì)象的類(lèi)(Page Class),是一件很耗費(fèi)時(shí)間和體力的事兒。
待研究方案:小程序頁(yè)面對(duì)象自動(dòng)生成,不用再手工維護(hù) Page Class ,只需要提供頁(yè)面路由,就會(huì)自動(dòng)生成這個(gè)頁(yè)面上控件的定位信息,并自動(dòng)生成 Page Class;
這些資料,對(duì)于【軟件測(cè)試】的朋友來(lái)說(shuō)應(yīng)該是最全面最完整的備戰(zhàn)倉(cāng)庫(kù),這個(gè)倉(cāng)庫(kù)也陪伴上萬(wàn)個(gè)測(cè)試工程師們走過(guò)最艱難的路程,希望也能幫助到你!

包括,測(cè)試人技術(shù)進(jìn)階路徑圖,50多天的視頻教程、16個(gè)項(xiàng)目實(shí)例,30多個(gè)測(cè)試工具,37份測(cè)試文檔,70個(gè)軟件測(cè)試相關(guān)問(wèn)題,40篇測(cè)試經(jīng)驗(yàn)級(jí)文章分享,還有軟件測(cè)試面試小程序,求職簡(jiǎn)歷的優(yōu)化模板。
加油吧,如果你需要提升技術(shù)儲(chǔ)備,那就行動(dòng),在路上總比在起點(diǎn)觀望的要好。一切的迷茫都是因?yàn)橄氲锰喽龅奶伲?/p>
你可以在公眾號(hào):傷心的辣條?! 自行領(lǐng)取一份216頁(yè)軟件測(cè)試工程師面試寶典文檔資料【免費(fèi)的】。以及相對(duì)應(yīng)的視頻學(xué)習(xí)教程免費(fèi)分享!,其中包括了有基礎(chǔ)知識(shí)、Linux必備、Shell、互聯(lián)網(wǎng)程序原理、Mysql數(shù)據(jù)庫(kù)、抓包工具專(zhuān)題、接口測(cè)試工具、測(cè)試進(jìn)階-Python編程、Web自動(dòng)化測(cè)試、APP自動(dòng)化測(cè)試、接口自動(dòng)化測(cè)試、測(cè)試高級(jí)持續(xù)集成、測(cè)試架構(gòu)開(kāi)發(fā)測(cè)試框架、性能測(cè)試、安全測(cè)試等。
現(xiàn)在我邀請(qǐng)你進(jìn)入我們的軟件測(cè)試學(xué)習(xí)交流群:【746506216】,備注“入群”, 大家可以一起探討交流軟件測(cè)試,共同學(xué)習(xí)軟件測(cè)試技術(shù)、面試等軟件測(cè)試方方面面,還會(huì)有免費(fèi)直播課,收獲更多測(cè)試技巧,我們一起進(jìn)階Python自動(dòng)化測(cè)試/測(cè)試開(kāi)發(fā),走向高薪之路。