軟件測試 | UI自動化常用設(shè)計(jì)模式
前言
接上一次的帖子,今天講一下我再 UI 自動化中常用的設(shè)計(jì)模式。 由于網(wǎng)上已經(jīng)有非常多的文章詳細(xì)講解了設(shè)計(jì)模式的編碼實(shí)現(xiàn),所以我今天也就不講實(shí)現(xiàn)細(xì)節(jié)了。 就是講我也講不出什么花來,只是網(wǎng)上的文章基本都是講解設(shè)計(jì)模式的本身實(shí)現(xiàn),很少針對某一領(lǐng)域的實(shí)際場景去講具體改怎么用設(shè)計(jì)模式。 所以今天我只針對一些實(shí)際的場景來說一下如何使用這些設(shè)計(jì)模式來完善 UI 自動化。
工廠
每種語言實(shí)現(xiàn)設(shè)計(jì)模式的方式都不一樣,這里僅以 java 為例。 一般來說,工廠模式是為了把創(chuàng)建一個對象的操作都集中在一起管理,其他所有需要用到這個對象的代碼都調(diào)用工廠類來創(chuàng)建對象。 在 UI 自動化中,工廠類有一個重要的作用就是提供數(shù)據(jù)的能力。 這里直接上一個例子, 在我的項(xiàng)目中有這樣一個場景, 我們的測試都分模塊的, 不同的模塊有不同的 QA。 測試模型中心模塊的 QA 想要測試的話就需要依賴建模 IDE 來產(chǎn)出各種各樣的模型。 那根據(jù)上一個帖子我講到的一個設(shè)計(jì)原則–模塊間有數(shù)據(jù)依賴的時候。每個模塊自己負(fù)責(zé)提供對外接口。 模型 IDE 的 QA 需要提供一個可以生產(chǎn)出各種不同模型的 API 來。 如下:

上面我們我們用一個簡單工廠來實(shí)現(xiàn)創(chuàng)建各種模型。 其他模塊調(diào)用此工廠方法滿足自己對模型的需求。 如果我們創(chuàng)建模型的類型更復(fù)雜的話,可以引入工廠模式和抽象工廠模式。 但實(shí)際上我最常用的還是簡單工廠,偶爾用工廠模式抽象工廠基本沒用過。使用設(shè)計(jì)模式的時候最容易出現(xiàn)的是過度設(shè)計(jì), 把過于復(fù)雜的模式硬搬到項(xiàng)目中來。 這是不可取的。
那接下來說一說這個工廠存在的意義吧。 簡單工廠算是設(shè)計(jì)模式里最簡單的了, 簡單到它幾乎不是一個什么模式。 它其實(shí)只有一種思想,就是把創(chuàng)建一個東西的操作都統(tǒng)一放到一起,調(diào)用方只需要知道我要一個東西,我需要把什么參數(shù)傳遞進(jìn)來就可以得到這個東西。 比如我們的這個例子里,調(diào)用方只需要傳遞我需要一個什么類型的模型的參數(shù)。 至于如何創(chuàng)建這個模型它不需要知道,里面包含了多復(fù)雜的 UI 操作它也不需要知道。 這樣做的好處是:
代碼復(fù)用,我們使用工廠的來創(chuàng)建的東西一般都是比較復(fù)雜的,需要很多的步驟才能創(chuàng)建。 如果只是隨便 new 一下就可以得到的對象也就犯不著專門搞個工廠方法了。 如果任由寫 case 的人根據(jù)自己的想法去創(chuàng)建這些對象,不僅造成了很多的重復(fù)代碼。 而且這些碎片的話的代碼在后期的維護(hù)上也是一個難以接受的事情。
封裝變化,我們把創(chuàng)建模型的所有操作都統(tǒng)一放在一起。之后生產(chǎn)模型的操作發(fā)生變化,比如需求變動。那我們只需要改動這一處就可以了。而且調(diào)用方也完全不感知
解耦,就如開始說的那個設(shè)計(jì)原則一樣, 調(diào)用方不感知復(fù)雜的模型生產(chǎn)過程, 達(dá)到解耦的作用。 在 UI 自動化中,尤其是業(yè)務(wù)邏輯特別復(fù)雜的大型項(xiàng)目中。 多人協(xié)作有個比較重要的點(diǎn)在這里提一下。 就是解耦,不要讓其他模塊的人感知自己模塊的任何實(shí)現(xiàn)細(xì)節(jié)。 他們了解的越少,操作的越少, 出錯的概率就越小,學(xué)習(xí)成本就越小。 畫地為界,分而治之。 其實(shí)我個人覺得整個設(shè)計(jì)模式就是在解決兩件事情:解耦和代碼復(fù)用
單例
我們有了上面的工廠方法來幫助我們創(chuàng)建模型, 但是這里有個問題。 就是我有太多的 case 依賴這些模型了。 如果每個 case 都執(zhí)行一遍上面的操作重新創(chuàng)建一個模型的話會有兩個問題:
UI 操作尤其耗時,尤其是生產(chǎn)模型這種異步操作
UI 本就不穩(wěn)定,這些重復(fù)的操作會增加 case 失敗的概率
所以我們希望除了有這種創(chuàng)建新模型的能力之外。 還能夠復(fù)用之前已經(jīng)產(chǎn)生的模型。 于是我們就有了使用單例模式的需求。 一般提到單例模式,基本上就是懶漢式,餓漢式什么的。 但這兩種大概率都是不可用的。 因?yàn)槭紫任覀兊牟僮魇茄舆t加載的,只有到了使用的時候才會去 UI 上執(zhí)行創(chuàng)建模型的操作。 總不能直接在類加載的時候就執(zhí)行吧。 至于在不加鎖的情況下判斷一下對象是否為 null 也是不行的。 因?yàn)楝F(xiàn)在的大規(guī)模 UI 自動化都是并發(fā)執(zhí)行的。 所以可選的方案就是加鎖的雙重檢查機(jī)制以及靜態(tài)內(nèi)部類了。 這里主要講一下靜態(tài)內(nèi)部類吧, 雙重檢查機(jī)制估計(jì)大家都玩爛了。 如下:

靜態(tài)內(nèi)部類不會再 LRModel 的類加載的時候就加載,而是有人調(diào)用 getInstance 的時候才會加載。所以保證了延遲加載
java 的 classloader 會保證靜態(tài)內(nèi)部類的線程安全,所以不用擔(dān)心并發(fā)的問題
上面是靜態(tài)內(nèi)部類的實(shí)現(xiàn)方式,優(yōu)點(diǎn)是相較于鎖的雙重檢查方來說實(shí)現(xiàn)起來簡單,坑少。 比如沒有那個經(jīng)典的指令重排序的問題。 當(dāng)然缺點(diǎn)也明顯, 就是一旦創(chuàng)建對象失敗, 那以后就再也沒有機(jī)會重新創(chuàng)建對象了。 而 UI 自動化又是出了名的不穩(wěn)定。 所以還是要慎重的。
模板
模板模式在 UI 自動化中比較常用的原因是在產(chǎn)品中有很多的操作路徑是復(fù)用的。 所以我們可以使用模板模式, 把固定的路徑抽象出來,由子類去實(shí)現(xiàn)那些獨(dú)立的邏輯。 比如:

上面是我們的產(chǎn)品引入一份數(shù)據(jù)的邏輯。 我們的數(shù)據(jù)引入有很多種類型。 比如從本地引入, 從數(shù)據(jù)庫引入,從 hdfs 引入,從 ftp 上引入等等等等。但是他們的基本步驟都是一樣的 (看截圖中的注釋), 所以模板模式的思想是使用父類來規(guī)定到執(zhí)行操作的步驟, 為了代碼復(fù)用所以也會實(shí)現(xiàn)一些通用的步驟比如所有的引入都得點(diǎn)擊某些 button,填寫一些都行。 然后留下一些 abstract 的方法給子類實(shí)現(xiàn)。 這種父類規(guī)定骨架,子類實(shí)現(xiàn)細(xì)節(jié)的方式就是模板方法了。 在這里我們的父類定義好了所有的步驟,但是部分的具體實(shí)現(xiàn)細(xì)節(jié)由子類完成。 這里我們發(fā)現(xiàn)子類需要實(shí)現(xiàn)兩個方法
每個數(shù)據(jù)引入的關(guān)于生成 table 的操作的 setTableConfig
每種數(shù)據(jù)引入的文件配置方式操作的 setFileConfig
當(dāng)然模板方法也是可以有較深的結(jié)構(gòu)的。 比如上面說的一些引入方式雖然都屬于數(shù)據(jù)引入,但是也分為兩大類, 一個是結(jié)構(gòu)化數(shù)據(jù),一個是圖片數(shù)據(jù)。 而且凡是屬于結(jié)構(gòu)化數(shù)據(jù)的引入方式有很多步驟都是相同的。 凡是屬于圖片數(shù)據(jù)引入的方式的大部分步驟也是相同的。 所以我們繼續(xù)有抽象類如下:

上面是結(jié)構(gòu)化數(shù)據(jù)的抽象類。 他實(shí)現(xiàn)了父類 IDataload 的 setTableConfig 方法。 因?yàn)樗薪Y(jié)構(gòu)化數(shù)據(jù)引入的這個頁面操作都是一樣的。然后才是我們具體的本地文件的數(shù)據(jù)引入的類。如下。

這個具體的本地文件引入的類實(shí)現(xiàn)了方法 setFileConfig。 這樣我們就看到了這個模板模式的全貌。
基類 IDataload 負(fù)責(zé)定義執(zhí)行步驟,以及個別 UI 操作的實(shí)現(xiàn)。 規(guī)定子類必須實(shí)現(xiàn) setTableConfig 和 setFileConfig 這兩個方法
類 StructureDataLoad 繼承基類 IDataload,并實(shí)現(xiàn)了 setTableConfig 方法。 因?yàn)樗械慕Y(jié)構(gòu)化數(shù)據(jù)引入在這里使用的是同樣的頁面
具體的實(shí)現(xiàn)類 LocalFileDataLoad 繼承 StructureDataLoad,代表著本地?cái)?shù)據(jù)引入并實(shí)現(xiàn)了針對于本地文件引入所獨(dú)有的頁面操作 setFileConfig
所以實(shí)際上調(diào)用方要做的事情就是這樣的

模板模式的優(yōu)點(diǎn):
代碼復(fù)用, UI 上很多操作路徑都是重復(fù)的,甚至說不同的業(yè)務(wù)流程操作中的部分頁面使用的是相同的頁面。 使用模板模式可以很好的整理我們的代碼結(jié)構(gòu),將業(yè)務(wù)邏輯分類并組織起來,可以服用的代碼由上層的父類實(shí)現(xiàn)。
模板模式的缺點(diǎn):
如果類層級結(jié)構(gòu)較多的時候,維護(hù)起來有點(diǎn)麻煩。
策略
策略模式也是非常常用的, 甚至很多時候它是其他模式的基礎(chǔ)。 它的思想也特別簡單。 當(dāng)初它誕生的原因是為了擺脫大量的 if else, 把每個條件分支做一個策略類。 具體原理我就不介紹了,不知道的可以 google 一下,網(wǎng)上一堆講設(shè)計(jì)模式的文章,我也講不出什么花來,我就講在 UI 自動化中我們怎么做。 舉一個最簡單的例子。如下:
在我們的測試中,大量的 case 都需要經(jīng)過如下的操作步驟:
打開瀏覽器
登錄
進(jìn)入模型 IDE 頁面
創(chuàng)建一個工程
創(chuàng)建一個 DAG
在 DAG 頁面上 build 一個 DAG
運(yùn)行 DAG 并等待運(yùn)行結(jié)束
既然大量的 case 都需要執(zhí)行上面的操作,那我們當(dāng)然就希望能做到代碼復(fù)用,所以就寫了一個方法來做這個事情。 但是我們發(fā)現(xiàn)這些步驟中有一個操作是無法預(yù)測的。 也就是如何 Build 一個 DAG, 我們的產(chǎn)品的 DAG 如下

每個 DAG 中都有不同的算子組合在一起,形成一個圖形。并且每個算子有它不同的配置。 要在 UI 上 build 一個 DAG 還是需要很多的操作的。 并且 case 之間要 build 的 DAG 的圖形也是不一樣的。 有的 case 需要 5 個算子組成一個圖形, 有的 case 可能需要 10 個算子組成一個圖形。 這些是完全不一樣的操作, 也就是說雖然我們想寫一個方法來封裝上面所有的操作。但是其中構(gòu)建 DAG 這一步是我們預(yù)先控制不了也復(fù)用不了的。這怎么辦? 所以我們索性把 build DAG 的操作定義為一個接口。 如下:

它只有一個方法,就是 build(), 意思是這個方法要實(shí)現(xiàn) build 一個 DAG 的操作。 但具體 build 一個什么圖形什么配置的 DAG, 由子類自己實(shí)現(xiàn)。
于是我們有了很多固定圖形的 dag 的子類, 他們分別實(shí)現(xiàn)不同的固定圖形的 build 操作。 如下:

于是我們創(chuàng)建這個可以用來復(fù)用的方法:

可以看到這個方法里我們執(zhí)行了上面說的所有的步驟,比如打開瀏覽器,登錄,跳轉(zhuǎn)頁面,創(chuàng)建工程等。 但是在 build 一個 dag 的時候,我們依賴一個 DagBuilder 類型的參數(shù),也就是我們之前的定義的那個接口,當(dāng)然這個 dagbuilder 使用了建造者模式,這個我們之后會講。 現(xiàn)在我們在 case 中就可以很愉快的使用很少量的代碼完成測試了。 如下:

當(dāng)然熟悉函數(shù)式編程的同學(xué)會覺得這玩意非常眼熟。 實(shí)際上在 java8 中也完全可以使用 lamda 表達(dá)式來完成 DagBuilder 的構(gòu)造
建造者
這里會涉及到建造者,策略和工廠三種模式的混合使用??赡軙容^啰嗦還請大家耐心看完。
建造者模式和工廠模式都是用來創(chuàng)建對象的。 建造者模式適用于一個對象的內(nèi)部有特別多的屬性需要外部來傳遞的情況。 比如在上一個說策略模式的例子中。我們把 Dagbuilder 作為策略類,在 case 調(diào)用的時候動態(tài)傳遞一個具體的 Dagbuilder 類型決定如何 build 一個 DAG. 那么剛才我們也看到了一個 DAG 是非常復(fù)雜的,里面有不同的圖形, 并且即便圖形固定了, 但是里面的算子的類型和配置可能都會變化。 比如,按照上面的一個通用的模型訓(xùn)練的 DAG 圖形, 我們就可以用下面的代碼來構(gòu)建。

可以看到上面每個一個 node 的 importToDag 的方法中都會有兩個 int 類型的數(shù)字參數(shù)。 這個意思是將算子拖拽到 DAG 中的哪一個點(diǎn)上。 并且 link 方法用來連接兩個算子, build 方法會執(zhí)行 UI 操作配置當(dāng)前算子。 通過這樣一段代碼就可以構(gòu)建出上面講策略模式的時候,截圖中的那個 DAG 圖形。 我們會發(fā)現(xiàn)非常多的 case 都會用到這個圖形。 比如測試所有的模型訓(xùn)練算法的時候, 都是走這個 DAG 圖形的。 所以我們理所應(yīng)當(dāng)?shù)臅氚堰@個圖形封裝起來給很多個 case 使用。 但是雖然 case 使用的圖形一樣,可是每個算子的配置可能是不一樣的, 而且可能在某一個節(jié)點(diǎn)上使用的算子都是不一樣的,這需要調(diào)用方動態(tài)的傳遞。 所以 builder(建造者) 模式是一個包含了很多個零件的對象, 它封裝了如何操作這些組件創(chuàng)造出最終調(diào)用方想要的東西。但是需要調(diào)用方自由的傳遞這些不同的零件給 builder。 首先我們看看這個 DAG 的 builder 類中定義要使用的零件。

上面是我們構(gòu)建這個模型訓(xùn)練雙輸入 DAG 所需要的零件。 可以看到由一個數(shù)據(jù)節(jié)點(diǎn),一個數(shù)據(jù)拆分算子,兩個特征抽取,一個模型訓(xùn)練,一個模型預(yù)測和一個模型預(yù)估組成。 而且這些零件都分別有 set 方法讓調(diào)用方來設(shè)置。然后我們就可以在 builder 的 build 方法里使用本節(jié)里一開始貼出的代碼來動態(tài)的構(gòu)建圖形了。
策略模式的混用
這里需要注意一點(diǎn)的是,這些零件大部分都是具體的實(shí)體類。 但是有些不是,比如模型訓(xùn)練算法,我們規(guī)定的是一個抽象類型。 如下:

為什么這么做呢,因?yàn)閷τ谒幸獪y試模型訓(xùn)練的 case 來說。 圖形是固定的, 某些算法也是固定的。 不論測試什么模型訓(xùn)練算法,都是一個數(shù)據(jù)下面連接數(shù)據(jù)拆分算法,再下面連接兩個特征抽取算法。 也就是說對于模型訓(xùn)練算法來說,這些流程都是固定的,我們實(shí)現(xiàn)就知道該拉取什么樣的算子,只是配置需要調(diào)用方動態(tài)傳遞。 但是測試的時候我們有各種不同的模型訓(xùn)練算法,這些可不是配置不同,而是連算子都變了, 所以我們把模型訓(xùn)練算法抽象成策略類。我不需要知道到底該拉取哪一個算子,讓調(diào)用方動態(tài)傳遞就好了。 只要它傳遞的是我規(guī)定的策略類型,有規(guī)定的方法來設(shè)置這個算子就可以了。
工廠模式的混用
根據(jù)上面的策略模式和建造者模式的混用我們就可以比較方便的構(gòu)建 DAG 圖形給 case 使用了。 但是還是有一點(diǎn)麻煩。那就是一個 builder 需要傳遞的零件太多了。這個體驗(yàn)有點(diǎn)不友好。 而且我們發(fā)現(xiàn)在大多數(shù)的模型訓(xùn)練測試場景下,我們只關(guān)心模型訓(xùn)練算法的配置參數(shù),而不是很在意其他算法的配置是什么樣子的。 這種場景下讓我一個一個的去傳遞這些零件還是有點(diǎn)麻煩。 或者說在有些情況下,我們是可以動態(tài)的推導(dǎo)出其他算子的配置的。 比如我這次要測試的是邏輯回歸這個算子。 那么邏輯回歸是一種二分類算子,那么其實(shí)它只能使用二分類的數(shù)據(jù),特征抽取算法中只能使用二分類的 label 處理, 相應(yīng)的下面也只能連接二分類算子的預(yù)測和評估算子。 這些都是我們可以動態(tài)推導(dǎo)出來的。 沒有必要讓使用者一個一個的去傳遞。所以我們在 builder 外面再包一層工廠, 一個創(chuàng)建 builder 的工廠。如下:

如上圖,根據(jù)傳遞的模型訓(xùn)練算子的類型找到預(yù)先導(dǎo)入的數(shù)據(jù),配置好特征抽取,推導(dǎo)出所有依賴的算子配置后。 配置好這個 builder 并返回給調(diào)用方。這樣我們通過之前講的 fastCreateDag 的策略模式的例子。 就可以在 case 中只寫入非常少量的代碼就完成了測試用例的編寫:
@Features(Feature.ModelIde)
? ?@Stories(Story.LR)
? ?@Description("GBDT雙輸入")
? ?@Test
? ?public void doubleInputGBDT(){
? ? ? ?fastCreateDag(Common.randomString("GBDT2Input"), DagBuilderFactory.getDoubleInputBuilder(new GBDTNode()))
? ? ? ? ? ? ? ?.run()
? ? ? ? ? ? ? ?.waitUntil(DagStatus.SUCCESS, 60*20);
? ?}
? ?@Features(Feature.ModelIde)
? ?@Stories(Story.LR)
? ?@Description("SVM雙輸入")
? ?@Test
? ?public void doubleInputSVM(){
? ? ? ?fastCreateDag(Common.randomString("SVM2Input"), DagBuilderFactory.getDoubleInputBuilder(new SVMNode()))
? ? ? ? ? ? ? ?.run()
? ? ? ? ? ? ? ?.waitUntil(DagStatus.SUCCESS, 60*20);
? ?}
? ?@Features(Feature.ModelIde)
? ?@Stories(Story.LR)
? ?@Description("hetreenet雙輸入")
? ?@Test
? ?public void doubleInputHeTreeNet(){
? ? ? ?fastCreateDag(Common.randomString("he2Input"), DagBuilderFactory.getDoubleInputBuilder(new HETreeNetNode()))
? ? ? ? ? ? ? ?.run()
? ? ? ? ? ? ? ?.waitUntil(DagStatus.SUCCESS, 60*20);
? ?}
? ?@Features(Feature.ModelIde)
? ?@Stories(Story.LR)
? ?@Description("gbrt雙輸入")
? ?@Test
? ?public void doubleInputGbrt(){
? ? ? ?fastCreateDag(Common.randomString("GBRT2Input"), DagBuilderFactory.getDoubleInputBuilder(new GBRTNode()))
? ? ? ? ? ? ? ?.run()
? ? ? ? ? ? ? ?.waitUntil(DagStatus.SUCCESS, 60*20);
? ?}
? ?@Features(Feature.ModelIde)
? ?@Stories(Story.LR)
? ?@Description("線性回歸雙輸入")
? ?@Test
? ?public void doubleInputLinearRegression(){
? ? ? ?fastCreateDag(Common.randomString("linearR"), DagBuilderFactory.getDoubleInputBuilder(new LinearRegressionNode()))
? ? ? ? ? ? ? ?.run()
? ? ? ? ? ? ? ?.waitUntil(DagStatus.SUCCESS, 60*20);
? ?}
? ?@Features(Feature.ModelIde)
? ?@Stories(Story.LR)
? ?@Description("線性分型回歸雙輸入")
? ?@Test
? ?public void doubleInputLFCRegression(){
? ? ? ?fastCreateDag(Common.randomString("LFCRe"), DagBuilderFactory.getDoubleInputBuilder(new LFCRegressionNode()))
? ? ? ? ? ? ? ?.run()
? ? ? ? ? ? ? ?.waitUntil(DagStatus.SUCCESS, 60*20);
? ?}
? ?@Features(Feature.ModelIde)
? ?@Stories(Story.LR)
? ?@Description("邏輯回歸多分類雙輸入")
? ?@Test
? ?public void doubleInputLRMultiClass(){
? ? ? ?fastCreateDag(Common.randomString("LRMulti"), DagBuilderFactory.getDoubleInputBuilder(new LRMultiClassNode()))
? ? ? ? ? ? ? ?.run()
? ? ? ? ? ? ? ?.waitUntil(DagStatus.SUCCESS, 60*20);
? ?}
可以看到上面的每一個模型訓(xùn)練的 case 的代碼量都非常的少。