深入学习FlexMonkey
本文简要介绍了FlexMonkey,并分析了在使用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 \u0026gt; Properties \u0026gt; 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 \u0026gt; View Application Tree窗口。该窗口根据自动化测试框架显示了测试下的应用组件树。正如你在图3所看到的,标签中的close按钮没有在树中出现。
\图 3:修改前的应用树
\Flex提供了三种方法来告知自动化框架组件树信息:getAutomationChildern()、getAutomationChildAt(index:int)和numAutomationChildren()。这些方法针对Flex组件而实现,但是在某些情况下,它无法正确识别树。对TerrificTabBar组件来说,像图4一样重载这些方法会让Flex正确识别组件树。
\\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; \} \\
图 4:Automation子方法的实现
\注意:automation子代码可以放在两种地方。它可以直接被添加到TerrificTabBarButton类,因为是UIComponent。也可以实现为自动化委托类配置到TerrificTabBarButton类。该代码逻辑上讲最适合用在委托类上,但是有时直接添加到UIComponent 类中更方便,比如它是唯一需要修改的代码。当你不能或者不想修改组件类源代码的时候,委托类是最佳选择。
\现在实现针对TerrificTabBarButton的定制委托类。为此,我创建了一个TerrificTabBarButtonDelegate类以扩展SparkToggleButtonAutomationImpl,在一个“delegates”包中,内容如图5所示。
\\package delegates { \\ 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 \u0026gt; Properties \u0026gt; Flex Compiler” 屏幕中添加如下标志位到编译器参数中:
\\-includes delegates.TerrificTabBarButtonDelegate\
三种automation子方法告诉自动化框架TerrificTabBarButton事实上拥有子元素,而不是标准的TabBarButton。标准的TabBar按钮没有子元素,因此getAutomationChildren()方法会返回一个空数组。通过修改代码,Flex自动化框架应该能够识别close按钮作为定制TabBar 按钮的子元素。
\步骤#2
\现在,代码更新以后,我重新加载应用,FlexMonkey的\"Project \u0026gt; 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添加以下代码到委托类中。
\\override protected function clickHandler( event:MouseEvent):void {\ 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所示。如你所见,代码非常类似于第一个压制事件的委托类。
\\override protected function clickHandler( event:MouseEvent):void {\ if(event.target.automationName != \"closeButton\")\ super.clickHandler(event);\ }\}\
图 9:TabBar定制委托类
\完成上一步,不要忘了在编译参数中添加新的委托类,因为Flex编译器只会包含在代码中引用的类:
\-includes delegates.TerrificTabBarDelegate
\步骤#4
\如我所愿,现在录制单击关闭按钮时,一切顺利。因此,我们正确的使用了委托类并防止了录制时不必要的事件发生。现在我可以继续下一步,自动化定制事件会允许我在定制的TabBar中录制和回放。
\修补#4 修改定制close事件
\像本文中的压制事件的情况并不常见。但是,修改定制的组件来配合FlexMonkey却很常见,不过这很容易实现。我会在委托类中为定制事件添加一个事件处理器,然后告诉委托类在事件发生时录制和回放。另一个需要做的事情是定制事件必须在FlexMonkeyEnv.xml环境文件中描述。
\首先,我创建一个定制的TerrificTabBarEvent 用于录制和回放。这样做的原因是TerrificTabBarEvent 类的构造函数需要定制参数。另一个定制事件的方法是在构造函数中为index属性设置默认值,通过setter方法使其可以修改。但是,在这里,我想展示一下如何不修改来完成自动化。
\使用图10中的内容,我在delegates.events包中创建了定制事件类TerrificTabBarEvent 。该事件允许FlexMonkey仅通过type值就实例化一个事件,然后在构造函数之外设置index属性。index属性是定制事件属性,告诉TerrificTabBar 关闭哪个标签。
\\package delegates.events {\ 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 \u0026gt; View Environment File选项,显示当前的文件内容,如图11所示。这可以用来确保定制的修改已经保存,或者访问现有的文件内容。为了创建自己的定制文件,我从屏幕中拷贝粘贴到FlexMonkeyEnv.xml 文件中,与MXML 应用源文件同目录。这会让Flash Builder拷贝它到bin目录下,FlexMonkey能够加载并使用它。
\图 11:FlexMonkey环境文件窗口
\一旦创建好文件,我添加了一些信息,如图12所示。这需要告诉自动化框架AutomationTerrificTabBarEvent 和它的属性。
\\\u0026lt;!-- Terrific Tab Bar --\u0026gt;\\u0026lt;ClassInfo Name=\"TerrificTabBar\" Extends=\"SparkListBase\"\u0026gt;\ \u0026lt;Implementation Class=\"components::TerrificTabBar\"/\u0026gt; \ \u0026lt;Events\u0026gt;\ \u0026lt;Event Name=\"CloseTab\"\u0026gt;\ \u0026lt;Implementation Class=\"delegates.events.AutomationTerrificTabBarEvent\" Type=\"closeTab\"/\u0026gt;\ \u0026lt;Property Name=\"index\"\u0026gt;\ \u0026lt;PropertyType Type=\"int\"/\u0026gt;\ \u0026lt;/Property\u0026gt;\ \u0026lt;/Event\u0026gt;\ \u0026lt;/Events\u0026gt;\\u0026lt;/ClassInfo\u0026gt;\
图 12:环境文件更新
\查看环境文件会比较费劲,因为初看起来你会以为需要为添加的定制委托类增加条目。但是,你可能注意到,我之前没有修改文件。这是因为,文件描述了一个层次组件,因此如果在父类描述中找不到它,那么条目不是必需的。理解这个原理的最好例子是查看FlexDisplayObject的条目,其描述了Click事件。因为所有的Flex组件扩展了UIComponent,自动化库有一个针对click事件的描述,无需在每一个条目上重复。
\接下来是添加逻辑代码到委托类中用于事件的录制和回放。在构造函数中,我们添加了事件监听器DisplayObject(TerrificTabBaButton的示例)。如图13所示,我更新了委托类的构造函数,添加了针对closeTab事件的事件监听器,并获得了对TabBar组件的私有引用。事件处理器优先级比默认值高1级,所以自动化框架会在标准处理器之前捕捉到事件。
\\private var _tabBar:TerrificTabBar;\public function TerrificTabBarDelegate(obj:TerrificTabBar) {\ super(obj);\ _tabBar = obj;\ obj.addEventListener(TerrificTabBarEvent.CLOSE_TAB, closeHandler, false, EventPriority.DEFAULT+1, true);\}\
图 13:添加委托事件处理器
\接下来,如图14所示,我实现了事件处理器。它告诉FlexMonkey使用之前创建的定制TerrificTabBarEvent录制事件。
\\protected function closeHandler( event:TerrificTabBarEvent):void {\ recordAutomatableEvent(new AutomationTerrificTabBarEvent(TerrificTabBarEvent.CLOSE_TAB, event.index));\}\\
图 14:委托事件处理器
\在设置委托类用于录制后,下一步就是如图15所示添加代码,告诉委托类如何回放事件,也就是把TerrificTabBarEvent分发给合适的组件用于获得期望的回放结果。
\\override public function replayAutomatableEvent(event:Event):Boolean {\ 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能够录制和回放该组件。\
资源
\- 在上文“调试阶段”所用到的最终Flash Builder项目压缩包,http://gorillajawn.com/flexmonkey/TerrificTabBar.zip\
- 你可以在Gorilla Logic公司的网站(http://www.gorillalogic.com/)上查看FlexMonkey的更多信息。\
- FlexMonkey项目官方博客:http://flexmonkey.org\
- 自动化框架的更多信息可以查看http://help.adobe.com/en_US/.../-7ec5.html\
关于作者
\Jon Rose是一位企业软件咨询师、位于Colorado州Boulder市的Gorilla Logic公司的Flex实践主管,他是企业软件社区InfoQ.com的编辑和贡献者。同时,他还是DrunkOnSoftware.com(针对同时喜欢饮酒和计算机的朋友们的videocast网站)的合伙人。他与大大小小的客户提供各种私人或者正式的咨询服务,对解决问题的执着促使他努力构建高质量的软件。他的博客是http://gorillajawn.com 。
\查看英文原文:FlexMonkey Deep Dive
\给InfoQ中文站投稿或者参与内容翻译工作,请邮件至[email protected]。也欢迎大家加入到InfoQ中文站用户讨论组中与我们的编辑和其他读者朋友交流。