本文简要介绍了 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 > 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 正确识别组件树。
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 > Properties > Flex Compiler” 屏幕中添加如下标志位到编译器参数中:
-includes delegates.TerrificTabBarButtonDelegate
三种 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 添加以下代码到委托类中。
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 > View Environment File 选项,显示当前的文件内容,如图 11 所示。这可以用来确保定制的修改已经保存,或者访问现有的文件内容。为了创建自己的定制文件,我从屏幕中拷贝粘贴到FlexMonkeyEnv.xml 文件中,与 MXML 应用源文件同目录。这会让 Flash Builder 拷贝它到 bin 目录下,FlexMonkey 能够加载并使用它。
图 11:FlexMonkey 环境文件窗口
一旦创建好文件,我添加了一些信息,如图 12 所示。这需要告诉自动化框架AutomationTerrificTabBarEvent 和它的属性。
<!-- Terrific Tab Bar --> <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 级,所以自动化框架会在标准处理器之前捕捉到事件。
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 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。
评论