深入學習FlexMonkey
什么是FlexMoneky?
FlexMonkey 是一款來自Gorilla Logic公司的開源工具,用于測試Flex和AIR應用。該項目包含了一個基于AIR的控制臺,通過提供錄制、回放和驗證Flex可視組件的功能支持用戶快速創建和運行用戶界面測試。FlexMonkey也允許用戶生成ActionScript版本的測試以適應持續集成環境。
你可以查看FlexMonkey的主頁:http://www.gorillalogic.com/flexmonkey
目標
FlexMonkey和底層的自動化框架通常運行良好,特別是對于常用Flex組件來說。但是有時,你可能碰上錄制和回放的問題,原因可能是定制的組件擴展、FlexMonkey的缺陷、Flex自動化框架的缺陷等。本文旨在幫助您了解如何快速地解決這類問題。
向你展示如何解決FlexMonkey問題時,我們將采用一個典型的調試過程,其中自動化一個定制組件的測試。為此,我們將使用Justin Shacklette的Flex 4 Terrific Tab Bar組件。Justin是Gorilla Logic的工程師,他廣泛使用Flex 4并在博客中介紹了許多有用的定制組件。Terrific Tab Bar組件在標準的Spark TabBar組件中添加了關閉按鈕。
正如你將在本文看到的,處理定制的TabBar的確需要花費一些精力,但是它并沒有初看起來那么令人畏懼。對于任何技術,你對它的原理了解得越深入,就越容易解決遇到的問題。
雖然處理自動化框架需要花費精力,但是值得這樣做,因為FlexMonkey允許你創建復雜的“開發者測試”(developer tests),而這些難以僅僅通過單元測試工具創建。在Gorilla Logic,我們已經不再通過工具層面來描述用戶界面測試任務,避免使用“單元測試”和“功能測試”這樣的字眼,而是選擇通過角色完成任務、使用類似“開發者測試”和“QA測試”這樣的描述。通過將它分解,我們能夠集中精力確保實現了不同的測試目標,但不讓開發人員局限于純粹的單元測試范疇,特別是當純粹的單元測試和用戶界面開發之間存在明顯的不匹配的時候。
盡管我們相信用戶界面的開發者測試從根本上與系統的其他部分不同,但是我們認為FlexMonkey沒有取代傳統的單元測試工具,如FlexUnit。FlexMonkey的部分代碼借鑒了傳統的單元測試(如業務邏輯等)。同時使用兩種工具會為開發人員提供創建健壯的開發者測試集的超級組合。
搭建環境
如果你想在自己的機器上實踐本文的內容,可以現在并導入Terrific Tab Bar的archive文件到Flash Builder。接下來,設置項目使用FlexMonkey。FlexMonkey控制臺會引導你完成這項工作,如圖1選擇AIR控制臺的Project > Properties > Setup Guide窗口。基本上有兩步:1)添加automation_monkey4.x.swc 到Flash Builder項目,2)更新編譯參數來包含FlexMonkey的Automation庫。一旦完成這些步驟,就應該能夠啟動Terrific TabBar應用和FlexMonkey控制臺了。你應該看到綠色連接燈亮起——這意味著FlexMonkey和應用正在通信,已經準備就緒。
圖 1:FlexMonkey配置向導
調試過程
為了針對Tab Bar組件做自動化測試,需要準備一些關聯工作。在這里,我們采用一套規范的嘗試和修補來使用FlexMonkey自動化該定制組件。因此,請跟隨我們, 希望在文章結束的時候你會明白如何通過本文講到的調試方法來修補自己的FlexMonkey問題。
如果你是FlexMonkey新手,可能需要首先閱讀一下我們最近的博文以了解FlexMonkey運行的基本原理。
步驟#1
首先,我們應該針對Terrific Tab Bar進行錄制工作。我首先嘗試單擊了一個tab標簽,看到FlexMonkey控制臺成功地錄制了一個“select”事件。這告訴我們 FlexMonkey搭建成功并且能夠錄制Spark TabBar的標準部分。接下來,我單擊close按鈕,失敗了,錯誤如圖2所示:
TypeError: Error #1009: Cannot access a property or method of a null object reference. At mx.automation::AutomationManager/isObjectChildOfSystemManagerProxy()[C:\work\flex\dmv_automation\projects\automation_agent\src\mx\automation\AutomationManager.as:2342] at mx.automation::AutomationManager/recordAutomatableEvent()[C:\work\flex\dmv_automation\projects\automation_agent\src\mx\automation\AutomationManager.as:2316] at mx.automation.delegates.core::UIComponentAutomationImpl/recordAutomatableEvent()[E:\SVN\4.x\frameworks\projects\automation\src\mx\automation\delegates\core\UIComponentAutomationImpl.as:387] at spark.automation.delegates.components.supportClasses::SparkButtonBaseAutomationImpl/clickHandler()[E:\dev\4.x\frameworks\projects\automation_spark\src\spark\automation\delegates\components\supportClasses\SparkButtonBaseAutomationImpl.as:129]
圖 2:AutomationManager的類型錯誤
修補#1 實現Automation子方法
處理這類來自自動化框架的錯誤最困難的部分是Adobe沒有對AutomationManager 類公開源代碼。因此,沒有簡便的方法找到錄制問題的根源。當自動化框架讓人困惑時,我通常首先驗證FlexMonkey和自動化框架能夠識別找到組件樹。
在最新版本的FlexMonkey中,在FlexMonkey控制臺中查看組件樹很容易。在連接上應用后,只需打開Project > View Application Tree窗口。該窗口根據自動化測試框架顯示了測試下的應用組件樹。正如你在圖3所看到的,標簽中的close按鈕沒有在樹中出現。
圖 3:修改前的應用樹
Flex提供了三種方法來告知自動化框架組件樹信息:getAutomationChildern()、 getAutomationChildAt(index:int)和numAutomationChildren()。這些方法針對Flex組件而實現,但是在某些情況下,它無法正確識別樹。對TerrificTabBar組件來說,像圖4一樣重載這些方法會讓Flex正確識別組件樹。
return [_tabButton.closeButton];
}
override public function getAutomationChildAt(index:int):IAutomationObject {
return getAutomationChildren()[index];
}
override public function get numAutomationChildren():int {
return 1;
}
圖 4:Automation子方法的實現
注意:automation子代碼可以放在兩種地方。它可以直接被添加到TerrificTabBarButton類,因為是UIComponent。也可以實現為自動化委托類配置到TerrificTabBarButton類。該代碼邏輯上講最適合用在委托類上,但是有時直接添加到UIComponent 類中更方便,比如它是唯一需要修改的代碼。當你不能或者不想修改組件類源代碼的時候,委托類是最佳選擇。
現在實現針對TerrificTabBarButton的定制委托類。為此,我創建了一個TerrificTabBarButtonDelegate類以擴展SparkToggleButtonAutomationImpl,在一個“delegates”包中,內容如圖5所示。
import components.TerrificTabBarButton;
import flash.display.DisplayObject;
import flash.events.MouseEvent;
import mx.automation.Automation;
import mx.automation.IAutomationObject;
import spark.automation.delegates.components.SparkToggleButtonAutomationImpl;
import spark.components.Button;
[Mixin]
public class TerrificTabBarButtonDelegate extends SparkToggleButtonAutomationImpl {
public static function init(root:DisplayObject):void {
Automation.registerDelegateClass(TerrificTabBarButton, TerrificTabBarButtonDelegate);
}
private var _tabButton:TerrificTabBarButton;
public function TerrificTabBarButtonDelegate(obj:TerrificTabBarButton) {
super(obj);
_tabButton = obj;
}
//
// implement automation methods so that Flex knows about about the close button
//
override public function getAutomationChildren():Array {
return [_tabButton.closeButton];
}
override public function getAutomationChildAt(index:int):IAutomationObject {
return getAutomationChildren()[index];
}
override public function get numAutomationChildren():int {
return 1;
}
}
}
圖 5:定制委托實現
為了使代碼編譯進應用中,我需要告知Flex編譯器,因為它代碼中沒有任何引用。在Flash Builder的“Project > Properties > Flex Compiler” 屏幕中添加如下標志位到編譯器參數中:
三種automation子方法告訴自動化框架TerrificTabBarButton事實上擁有子元素,而不是標準的TabBarButton。標準的TabBar按鈕沒有子元素,因此getAutomationChildren()方法會返回一個空數組。通過修改代碼,Flex自動化框架應該能夠識別close按鈕作為定制TabBar 按鈕的子元素。
步驟#2
現在,代碼更新以后,我重新加載應用,FlexMonkey的"Project > View Application Tree”窗口顯示的結果更好一些。圖6顯示了FlexMonkey現在正確地顯示了組件樹,每一個tab都有關閉按鈕。
圖 6:更新Automation子方法后的應用組件樹
接下來,我嘗試再一次錄制關閉按鈕。不幸的是,圖1的錯誤依然存在。
即使重載automation子方法也無法解決這個定制TabBar的問題,這仍然是自動化框架FlexMonkey難題的重要問題之一。許多自動化問題可以通過處理這些方法來解決。值得一提的是這不并總是能夠幫助自動化管理器識別其無法發現的組件。有時,它只是能夠通過重載這些方法來有效地從自動化管理器中隱藏組件。 這需要你根據實際情況來判斷決定。憑借新的FlexMonkey “Application Automation Tree” 界面,很容易看出自動化框架識別的東西。大多數情況下,采用哪種方法來識別事物是很明顯的。
修補#2:從Automation Manager隱藏事件
現在,讓我仔細看一下棧跟蹤。AutomationManager 類的源代碼是看不到的,但是Adobe提供了委托類的源代碼。因此,調試這些代碼可以更好的發現錄制過程失敗的原因。
要添加源代碼,單擊文件窗口(點擊棧跟蹤中的委托類會打開)中的 “Edit Source Lookup Path…” 按鈕。委托類源代碼可以在“[SDK HOME]/frameworks/projects”目錄中找到。那個目錄存在多個項目。針對棧跟蹤中的委托類,我添加了如下目 錄:“automation/src”和“automation_spark”。
為了理解運行原理,我調試了SparkButtonBaseAutomationImpl的“clickHandler”方法,檢查事件,如圖7所示。事件看起來像期望的那樣,但是我注意到它還有一個“target”是spark.components.Button,而我希望是關閉按鈕的類型。
圖 7:調試委托類
如果我再次調試該方法,但是單擊標簽部分,而不是關閉按鈕,事件的target是Label而不是Button。當我沒有相關信息(如AutomationManager源代碼)與自動化框架作斗爭時,我就會壓制這個錯誤以免其影響自動化框架。為此,我在TerrificTabBarButtonDelegate類(在修補#1中創建的)中重載clickHandler方法,如圖8添加以下代碼到委托類中。
if(event.target.automationName != "closeButton")
super.clickHandler(event);
}
}
圖 8:重載clickHandler壓制事件
clickHandler重載方法檢查了事件的目標,如果不是關閉按鈕,它就只調用父類實現。
步驟#3
我重新錄制關閉按鈕。結果,離成功又接近了一步!錯誤不再拋出。但是,在單擊關閉按鈕時,FlexMonkey錄制了Select事件,當我們回放時,它只選擇標簽,而不是關閉按鈕。
修補#3:在TabBar組件中壓制額外的Select事件
現在,雖然事情沒有好轉,但是代碼已經修改的不錯,因為之前的錯誤已經移除了。我準備創建另一個委托類。該委托類將針對定制的TabBar。類似于TerrificTabBarButtonDelegate 的實現,我會壓制在關閉按鈕被單擊時導致Select動作的事件。然后,在修補#4中,自動化定制的TerrificTabBarEvent 用于錄制和回放,這是為了讓組件與FlexMonkey協作修改的最后一部分。
重復修補#1中的相同步驟,創建另一個委托類TerrificTabBarDelegate。針對新代理類的源代碼如圖9所示。如你所見,代碼非常類似于第一個壓制事件的委托類。
if(event.target.automationName != "closeButton")
super.clickHandler(event);
}
}
圖 9:TabBar定制委托類
完成上一步,不要忘了在編譯參數中添加新的委托類,因為Flex編譯器只會包含在代碼中引用的類:
步驟#4
如我所愿,現在錄制單擊關閉按鈕時,一切順利。因此,我們正確的使用了委托類并防止了錄制時不必要的事件發生。現在我可以繼續下一步,自動化定制事件會允許我在定制的TabBar中錄制和回放。
修補#4 修改定制close事件
像本文中的壓制事件的情況并不常見。但是,修改定制的組件來配合FlexMonkey卻很常見,不過這很容易實現。我會在委托類中為定制事件添加一個事件處理器,然后告訴委托類在事件發生時錄制和回放。另一個需要做的事情是定制事件必須在FlexMonkeyEnv.xml環境文件中描述。
首先,我創建一個定制的TerrificTabBarEvent 用于錄制和回放。這樣做的原因是TerrificTabBarEvent 類的構造函數需要定制參數。另一個定制事件的方法是在構造函數中為index屬性設置默認值,通過setter方法使其可以修改。但是,在這里,我想展示一下如何不修改來完成自動化。
使用圖10中的內容,我在delegates.events包中創建了定制事件類TerrificTabBarEvent 。該事件允許FlexMonkey僅通過type值就實例化一個事件,然后在構造函數之外設置index屬性。index屬性是定制事件屬性,告訴TerrificTabBar 關閉哪個標簽。
import flash.events.Event;
public class AutomationTerrificTabBarEvent extends Event {
public var index:int;
public function AutomationTerrificTabBarEvent(type:String, index:int=-1, bubbles:Boolean=false, cancelable:Boolean=false) {
super(type, bubbles, cancelable);
this.index = index;
}
override public function clone():Event {
return new AutomationTerrificTabBarEvent(type,index,bubbles,cancelable);
}
}
}
圖 10:更新事件類
接下來,我們會修改FlexMonkey環境文件來告知其事件。最新版的FlexMonkey增加了一個工具窗口Project > View Environment File選項,顯示當前的文件內容,如圖11所示。這可以用來確保定制的修改已經保存,或者訪問現有的文件內容。為了創建自己的定制文件,我從屏幕中拷貝粘貼到FlexMonkeyEnv.xml 文件中,與MXML 應用源文件同目錄。這會讓Flash Builder拷貝它到bin目錄下,FlexMonkey能夠加載并使用它。
圖 11:FlexMonkey環境文件窗口
一旦創建好文件,我添加了一些信息,如圖12所示。這需要告訴自動化框架AutomationTerrificTabBarEvent 和它的屬性。
<ClassInfo Name="TerrificTabBar" Extends="SparkListBase">
<Implementation class="components::TerrificTabBar"/>
<Events>
<Event Name="CloseTab">
<Implementation class="delegates.events.AutomationTerrificTabBarEvent" Type="closeTab"/>
<Property Name="index">
<PropertyType Type="int"/>
</Property>
</Event>
</Events>
</ClassInfo>
圖 12:環境文件更新
查看環境文件會比較費勁,因為初看起來你會以為需要為添加的定制委托類增加條目。但是,你可能注意到,我之前沒有修改文件。這是因為,文件描述了一個層次組件,因此如果在父類描述中找不到它,那么條目不是必需的。理解這個原理的最好例子是查看FlexDisplayObject的條目,其描述了Click事件。因為所有的Flex組件擴展了UIComponent,自動化庫有一個針對click事件的描述,無需在每一個條目上重復。
接下來是添加邏輯代碼到委托類中用于事件的錄制和回放。在構造函數中,我們添加了事件監聽器DisplayObject(TerrificTabBaButton的示例)。如圖13所示,我更新了委托類的構造函數,添加了針對closeTab事件的事件監聽器,并獲得了對TabBar組件的私有引用。事件處理器優先級比默認值高1級,所以自動化框架會在標準處理器之前捕捉到事件。
public function TerrificTabBarDelegate(obj:TerrificTabBar) {
super(obj);
_tabBar = obj;
obj.addEventListener(TerrificTabBarEvent.CLOSE_TAB, closeHandler, false, EventPriority.DEFAULT+1, true);
}
圖 13:添加委托事件處理器
接下來,如圖14所示,我實現了事件處理器。它告訴FlexMonkey使用之前創建的定制TerrificTabBarEvent錄制事件。
recordAutomatableEvent(new AutomationTerrificTabBarEvent(TerrificTabBarEvent.CLOSE_TAB, event.index));
}
圖 14:委托事件處理器
在設置委托類用于錄制后,下一步就是如圖15所示添加代碼,告訴委托類如何回放事件,也就是把TerrificTabBarEvent分發給合適的組件用于獲得期望的回放結果。
if(event is AutomationTerrificTabBarEvent) {
return _tabBar.dataGroup.dispatchEvent(new TerrificTabBarEvent(TerrificTabBarEvent.CLOSE_TAB, (event as AutomationTerrificTabBarEvent).index));
} else {
return super.replayAutomatableEvent(event);
}
}
圖 15:委托回放邏輯代碼
步驟#5
最終,我可以成功的錄制和回放定制的TabBar了!
解決方案
隨著錄制和回放都運轉正常,我們應該回顧一下解決方案的過程。最后,我只修改了兩個委托類、一個定制事件類和FlexMonkey環境文件的小部分代碼。下面是我的總結:
- 重載自動化子方法,使FlexMonkey和自動化框架能夠識別每一個標簽上的關閉按鈕。
- 防止close按鈕點擊事件被FlexMonkey自動化框架分發到ButtonBarButton上,因為它會導致AutomationManager類的一個錯誤。
- 防止close按鈕點擊事件被FlexMonkey自動化框架分發到TabBar上,因為它會被錯誤地當做Select事件被錄制。
- 在TerrificTabBarEventDelegate中自動化定制的TerrificTabBarEvent,確保FlexMonkey能夠錄制和回放該組件。