Author: Shashank Tiwari & Elad Elrom
Translator: 李学锟
Chapter 1: 使用测试驱动开发模式创建应用程序....................................................... 1
FlexUnit 4 概述.........................................................................................................................................2
编写第一个套件..........................................................................................................................................2
编写第一个测试用例类 ..............................................................................................................................5
检查结果 ...................................................................................................................................................7
使用FlexUnit4进行测试驱动开发模式 .....................................................................................................25
小结..........................................................................................................................................................42
在Flex 4 中,有一个焦点的技术就是引入了应用程序的设计中心的开发,它主要是将表示层和逻辑层区别出来,并将表示层的工作交给使用Flash Catalyst 的设计者。给开发者减少这方面的责任是为了开发者创建出更敏捷、更动态和更复杂的Flash应用程序。
但是,随着Flash应用程序成为更复杂和更动态的同时,业务需求也在较快地变更,甚至有时是在开发阶段;这样就给Flash应用程序的维护或更新带来了挑战。这挑战是很有影响的,许多Flash开发者发现使用框架也不太容易维护和升级应用程序。这样的挑战在各种开发中都很常见:移动设备、基于Web和基于桌面系统的。
考虑下面的问题:一个大应用程序需要变更因为新的业务需求导致。你怎么能知道你一个小的改动不会影响程序的其它部分呢?你怎么能确保代码都是牢靠的,并且有些代码不是你写的?
这个问题对软件工程师来说并不是一个新问题;Java 和ASP开发者早已挑战过这样的问题,他们发现了一个很有用的方式--测试驱动开发模式(TDD)来创建程序,这样便于程序的维护。
Flash 从一个小的动画工具成长为一个真正的程序语言,和其它的语言一样需要公共的方法来创建大型的动态应用程序。事实上,Adobe和其它公司都发现使用TDD来解决大多数需求变更,存在于开发者每天的开发周期中。
就个人而言, 我相信许多开发者都听说过TDD;但是,其中有些人不愿使用它的原因是不知道如何使用和担心使用TDD会增加开发时间。
从我个人的经历来看,我发现正确地使用TDD并不会增加开发时间。事实上,你会减少开发时间并使程序在较长时间内便于维护。另外我发现你可以实现并使用TDD在现在的应用程序上,即使程序使用了其它的框架如Cairgorm或Robotlegs。
TDD是可行的,即使是有质量保证(QA)部门的环境下,因为它提供了更稳固的代码供QA来创建他们需要的测试用例在用户界面上进行测试。
在本章中,将要讲解一些基本的内容及较高级的话题如:在现在程序中怎样开始使用TDD和如何为复杂的类创建单元测试,这样包括服务的调用及复杂的逻辑。
FlexUnit 4 概述
在冲入TDD之前,我们先来了解一下FlexUint 4,因为将要用到FlexUnit 4来创建测试。
让我们回顾一下它的成长历程。2003年Adobe并购了一个咨询公司,后来成为了Adobe的咨询公司,它们发布了AS2Unit这个产品。后来很短的时间内,Adobe发布了Flex1 和 AS2Unit 被升级为FlexUnit 在2004年。
直到Flash Builder 4的发布之前,你必须下载SWC,人工地创建测试用例(Test Case)和测试套件(Test Suite)。随着Flash Builder 4的发布,Adobe 添加了FlexUnit 插件作为向导的一部分,并使Flash Builder更容易地它。该插件自动实现许多手动执行的任务,简化了单位测试的过程。这个工程早期发布了Flex Unit 0.9 。但后来的一个版本就变成了FlexUnit 1。
最新的版本FlexUnit 4 的功能接近于JUnit(http://www.junit.org/)工程,支持JUnit的许多特点。FlexUnit 4还兼容了早期的FlexUnit 1.0和 Fluint(http://code.google.com/p/fluint/)工程。
一些FlexUnit 4 的主要特点如下:
• 易于创建测试套件和测试用例类
• 易于创建Test Runner和 整合其它框架的runners
• 更好地使用持续集成
• 更好地处理异步测试
• 更好地处理异常
• 框架是标签驱动
• 允许用户界面测试
• 具有创建测试序列的能力
编写你的第一个测试套件
1.打开Flash Builder 4,选择File(文件) ➤ New(新建) ➤ Flex Project(Flex工程)。将工程命名为FlexUnit4App 并选择Finish(完成)。
2.点击刚创建的工程并选择File(文件) ➤ New(新建) ➤ Test Suite Class(测试套件类)。
在弹出的窗口中,你可设置它的名称和选择它所包括的任何test。定义该suite(套件)为FlexUnit4AppSuite,然后点击Finish(完成)。
一个测试套件是一组测试。它运行一个集合的测试用例。在开发过程中,你可以创建一个测试的集合在一个测试套件下。一旦你完成某些需要的更改,你可以来运行测试套件来确保你的代码在更改后是正常工作的。
向导创建了一个flexUnitTests包和FlexUnit4AppSuite.as 类(如图1-3)
打开你刚创建的FlexUnit4AppSuite 类:
备注:你使用了Suite 标签,它表示该类是一个Suite(套件) 。RunWith 标签是使用FlexUnit4来表示runner将来一块来执行的代码。
FlexUnit 4是runners的一个集合,它来运行创建一个测试的完整的设置。你可以定义每个runner来实现一个特定的接口。例如,你可以选择在运行测试时指向一个类来代替FlexUnit4默认创建的类。
这表明框架是足够灵活地支持将来的runners和允许开发者创建自己的runners,而且使用同一的UI。事实上,目前有FlexUnit 1,FlexUnit 4,Fluint和SLT的runner。
编写你的第一个测试用例类
1.选择File(文件) ➤ New(新建) ➤ Test Case Class(测试用例类)。命名为FlexUnitTester ➤
flexUnitTests. 点击Finish(完成)。
向导自动创建了FlexUnitTester.as 类,如下的代码:
注意在创建测试用例类的窗口中,你可以关联一个类去测试,如上图。这样的做法是针对已有代码做测试时比较好用。你可以在New Test Case Class(新建测试用例类)窗口中选择 Next 来代替Finish。在这之前,你要先勾选 Select class to test 然后通过 Browse 按钮来浏览选择要测试的类;
这时 Next 才是可用的。
你必须向已创建的测试套件里添加测试用例类。完成这一步,只是添加引用就可以了。如下:
现在你可以运行这个测试了。选择Run图标并在下拉菜单中选择 FlexUnit Tests ,如图
查看结果
Flash Builder 将会打开一个浏览器的窗口显示运行测试的信息和显示测试的结果。如图:
关闭浏览器的测试结果信息,来看一下IDE中的FlexUint测试结果显示窗口中的信息。测试失败的原因是没有一个可运行测试的方法,没有创建任何被测试的方法对象。
将现在的FlexUnitTester.as的代码替换成下面的代码,关于下面代码的含义,我们将在下一节中讲解:
package flexUnitTests
{
import flash.display.Sprite;
import flexunit.framework.Assert;
public class FlexUnitTester
{
//--------------------------------------------------------------------------
//
// Before and After
//
//--------------------------------------------------------------------------
[Before]
public function runBeforeEveryTest():void
{
// implement
}
[After]
public function runAfterEveryTest():void
{
// implement
}
//--------------------------------------------------------------------------
//
// Tests
//
//--------------------------------------------------------------------------
[Test]
public function checkMethod():void
{
Assert.assertTrue( true );
}
[Test(expected="RangeError")]
public function rangeCheck():void
{
var child:Sprite = new Sprite();
child.getChildAt(0);
}
[Test(expected="flexunit.framework.AssertionFailedError")]
public function testAssertNullNotEqualsNull():void
{
Assert.assertEquals( null, "" );
}
[Ignore("Not Ready to Run")]
[Test]
public function methodNotReadyToTest():void
{
Assert.assertFalse( true );
}
}
}
再次运行FlexUnit4,在IDE的FlexUnit4 结果窗口中,看到了绿灯;代表Test全部正确。
FlexUnit4 是基于标签的,来看下面一些常用的标签:
• [Suite]: 表示该Class是一个套件类.
• [Test]: Test 标签替换测试方法的前缀 支持expected, async, order, timeout, and ui 属性。
• [RunWith]: 用于选择要使用的runner。
• [Ignore]: 在方法前添加Ignore 标签来代替注释方法。
• [Before]: 替换FlexUnit1的setup()方法,允许多个方法同时使用;支持async, timeout, order,和ui 属性。
• [After]: 替换FlexUnit1的teardown()方法,允许多个方法同时使用;支持async, timeout, order,和ui 属性。
• [BeforeClass]: 表示在测试类之前执行的方法, 支持order 属性。
• [AfterClass]: 表示在测试类之后执行的方法, 支持order 属性。
正如在例子中,你使用了许多标签,比如RangeError, AssertionFailedError,和Ingore标签,使用这些标签使编码变得容易。接下来我们要讲解这些代码。
断言方法
回到上面的那个例子:Before 和 After 标签表示这些方法将在所有测试方法之前和之后运行。
[Before]
public function runBeforeEveryTest():void
{
// implement
}
[After]
public function runAfterEveryTest():void
{
// implement
}
Test 标签替换每个方法的前缀,让你有一种可以不和测试一块启动的办法。
[Test]
public function checkMethod():void
{
Assert.assertTrue( true );
}
在FlexUnit1中,你必须将不需要测试的方法给注释掉。现在FlexUnit4只需要在该方法前添加Ignore 标签就可跳过该方法运行了。
[Ignore("Not Ready to Run")]
[Test]
public function methodNotReadyToTest():void
{
Assert.assertFalse( true );
}
注:在某种情况下,你希望创建系列有先后序列的方法来测试时,你可以通过添加order属性来完成。如:[Test(order=1)]
在创建Test Class时你可能还会用到其它的断言方法,请看表1-1:
表1-1. Asserts 类的方法和描述
Assert type | Description |
assertEquals | 假设2个值相等 |
assertContained | 假设第1个字符串包含第2个字符串 |
assertNoContained | 假设第1个字符串不包含第2个字符串 |
assertFalse | 假设该条件是错误的 |
assertTrue | 假设该条件是正确的 |
assertMatch | 假设1个字符串满足1个正则表达式 |
assertNoMatch | 假设1个字符串不满足1个正则表达式 |
assertNull | 假设一个对象为空 |
assertNotNull | 假设一个对象不为空 |
assertDefined | 假设一个对象已定义声明 |
assertUndefined | 假设一个对象未定义声明 |
assertStrictlyEquals | 假设两个对象严格相同 |
assertObjectEquals | 假设2个对象相等 |
使用一个假设方法,传入一个字符串信息和两个要比较的参数。这个字符串信息只有在test失败时才会被用到。如下:
[Test]
public function testAsserEquals():void
{
var state:int = 0; //state应该是App具体要test的变量;在这里为了说明问题,在function内部定义了它。
assertEquals("Error testing the application state",state,1);
}
不过在通常情况下,是不需要传入字符串信息的。
异常处理
Test标签允许定义异常属性,用来测试异常的情况。工作方式是测试方法的expected 属性指向你期望出错的错误信息,一旦该异常出现,测试将通过。
接下来的例子是演示 Test 标签的expected 属性。rangeCheck 方法创建一个新的Spirt 对象。代码将成功地测试通过,因为index 为1的子类不存在,同时在运行时将有异常信息。
[Test(expected="RangeError")]
public function rangeCheck():void
{
var child:Sprite = new Sprite();
child.getChildAt(0);
}
另外一个例子期望是一个假设错误。来回顾一下testAsserNullNotEqualsNull方法,该方法期望出现AssertionFailedError 失败的错误。assertEquals方法代码将是不通过的,因为null不等于"" ,所以假设是失败的。
当表达式变为:Assert.assertEquals( null, null ); 然后你将得到成功的测试。
[Test(expected="flexunit.framework.AssertionFailedError")]
public function testAssertNullNotEqualsNull():void
{
Assert.assertEquals( null, null );
}
Test Runners
来查看一下自动生成的FlexUnitApplication.mxaml文件,这是test程序的主入口。
<?xml version="1.0" encoding="utf-8"?>
<!-- This is an auto generated file and is not intended for modification. -->
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx"
minWidth="955" minHeight="600"
xmlns:flexui="flexunit.flexui.*"
creationComplete="onCreationComplete()">
<fx:Script>
<![CDATA[
import flexUnitTests.FlexUnit4AppSuite;
public function currentRunTestSuite():Array
{
var testsToRun:Array = new Array();
testsToRun.push(flexUnitTests.FlexUnit4AppSuite);
return testsToRun;
}
private function onCreationComplete():void
{
testRunner.runWithFlexUnit4Runner(currentRunTestSuite(), "FlexUnit4App");
}
]]>
</fx:Script>
<fx:Declarations>
<!-- Place non-visual elements (e.g., services, value objects) here -->
</fx:Declarations>
<flexui:FlexUnitTestRunnerUI id="testRunner">
</flexui:FlexUnitTestRunnerUI>
</s:Application>
testRunner是FlexUnitTestRnnerUI 类的对象,一旦测试的App创建完成,就会调用onCreationComplete()方法,该方法是testRunner调用自己的runWithFlexUnit4Runner(test:Array, projectName:String, contextName:String="", onComplete:Function=null):void
它有4个参数,前面2个是必填的:要测试的套件类,要测试的工程名称。
所以在这里,我们的第1个参数应该是包含FlexUnit$AppSuit类的数组,第2个就是我们的工程名称:
FlexUnit4App 。
Hamcrest 断言方法
除了这些Assert.assertEquals,Assert.assertFalse这些标准的断言方法,FlexUnit4还支持Hamcrest断言方法,这要归功于Hamcrest(http://github.com/drewbourne/hamcrest-as3 )。 Hamcrest 是基于匹配功能的类库,允许定义匹配的规则。在假设方法里每个匹配者将要与匹配的条件进行匹配。
创建一个FlexUnitCheckRangeTester测试类:
package flexUnitTests
{
import org.flexunit.assertThat;
import org.hamcrest.collection.hasItem;
import org.hamcrest.core.allOf;
import org.hamcrest.number.between;
import org.hamcrest.number.closeTo;
import org.hamcrest.object.equalTo;
public class FlexUnitCheckRangeTester
{
//--------------------------------------------------------------------------
//
// Before and After
//
//--------------------------------------------------------------------------
private var numbers:Array;
[Before]
public function runBeforeEveryTest():void
{
numbers = [1, 2, 3, 4];
}
[After]
public function runAfterEveryTest():void
{
numbers = null;
}
//--------------------------------------------------------------------------
//
// Tests
//
//--------------------------------------------------------------------------
[Test]
public function shouldDemonstrateHamcrestInTests():void
{
assertThat(numbers, allOf(hasItem(equalTo(3)), hasItem(closeTo(5, 1))));
}
[Ingore]
[Test]
public function shouldDemonstrateHamcrestDescriptions():void
{
numbers = [1, 2, 3, 7, 8, 9];
assertThat(numbers, allOf(hasItem(equalTo(3)), hasItem(closeTo(5, 1))));
}
}
}
在讲解上面这个例子前,我们得先学习几个方法:
1.equalTo(value:Object):Matcher
检查是否相等,若被检查的对象是数组,则先检查长度是否相等,及每一项是否相等。
用法如: assertThat("hi", equalTo("hi"));
assertThat("bye", not(equalTo("hi")));
2.closeTo(value:Number, delta:Number):Matcher
检查一个给定值加或减去 浮动值 是否等于 vlaue参数的值
用法如:
assertThat(3, closeTo(4, 1));
// 通过
assertThat(3, closeTo(5, 0.5));
// 失败
assertThat(4.5, closeTo(5, 0.5));
// 通过
3.hasItem(value:Object):Matcher
如果匹配的项是一个数组,则应包含给定匹配中的一项。
用法如:assertThat([1, 2, 3], hasItem(equalTo(3));
3.allOf(...rest):Matcher
检查是否全包含给定的匹配项。
用法如:assertThat("good", allOf(equalTo("good"), not(equalTo("bad"))));
所以shouldDemonstrateHamcrestInTests()方法可以测试通过;shouldDemonstrateHamcrestDescriptions()方法是失败的。
异步测试
也许你以前用过FlexUnit 1,那么你就知道当进行异步测试或事件驱动的代码时是多么地不方便。Flunit的一个最好的优势就是有能力招待多个异步事件。FlexUnit4 结合Flunit 的这个功能,它增加了异步测试,包括异步的启动和折卸。
创建一个名为AsynchronousTester的测试类:
package flexUnitTests
{
import flash.events.Event;
import flash.events.EventDispatcher;
import flexunit.framework.Assert;
import mx.rpc.events.FaultEvent;
import mx.rpc.events.ResultEvent;
import mx.rpc.http.HTTPService;
import org.flexunit.async.Async;
public class AsynchronousTester
{
private var service:HTTPService;
//------------------------------------------------------------
//
// Before and After
// //------------------------------------------------------------
[Before]
public function runBeforeEveryTest():void
{
service = new HTTPService();
service.resultFormat = "e4x";
}
[After]
public function runAfterEveryTest():void
{
service = null;
}
//------------------------------------------------------------
//
// Tests
// //------------------------------------------------------------
[Test(async,timeout="3000")]
public function testServiceRequest():void
{
service.url = "assets/file.xml";
service.addEventListener(ResultEvent.RESULT,
Async.asyncHandler(this, onResult, 500 ), false, 0, true );
service.send();
}
[Test(async,timeout="500")]
public function testeFailedServicRequest():void
{
service.url = "file-that-dont-exists";
service.addEventListener( FaultEvent.FAULT,
Async.asyncHandler( this, onFault, 500 ), false, 0, true );
service.send();
}
[Test(async,timeout="3000")]
public function testEvent():void
{
var EVENT_TYPE:String = "eventType";
var eventDispatcher:EventDispatcher = new EventDispatcher();
eventDispatcher.addEventListener(EVENT_TYPE,
Async.asyncHandler( this, handleAsyncEvnet, 300 ), false, 0, true );
eventDispatcher.dispatchEvent( new Event(EVENT_TYPE) );
}
[Test(async,timeout="6000")]
public function testMultiAsync():void
{
testEvent();
testServiceRequest();
}
//------------------------------------------------------------
//
// Asynchronous handlers
// //------------------------------------------------------------
private function onResult(event:ResultEvent, passThroughData:Object):void
{
Assert.assertTrue( event.hasOwnProperty("result") );
}
private function handleAsyncEvnet(event:Event, passThroughData:Object):void
{
Assert.assertEquals( event.type, "eventType" );
}
private function onFault(event:FaultEvent, passThroughData:Object):void
{
Assert.assertTrue( event.fault.hasOwnProperty("faultCode") );
}
}
}
另外,我们需要在src目录下新建 assets/file.xml 文件。只要符合XML格式的文件都可以。
如下:
<?xml version="1.0" encoding="utf-8"?>
<nodes>
<node state='unchecked' label='S_GLC1' value=''>
<node state='unchecked' label='Blance Sheet' value=''>
<node state='unchecked' label='Fixed Assets' value='10'/>
<node state='unchecked' label='Investments' value='11'/>
<node state='unchecked' label='Current Assets' value='12'/>
<node state='unchecked' label='Other Assets' value='13'/>
<node state='unchecked' label='Liabilities' value='14'/>
<node state='unchecked' label='Capital Reserves' value='15'/>
</node>
</node>
</nodes>
[Before]标签表明该方法运行在所有test方法运行之前,[After] 标签表明在类中所有test方法运行完成再运行该方法。为了避免内存的浪费,在做完该test类后,应该有一个方法将service置成null。
在Test 标签里可以添加async 属性,来进行异步的测试,并且设置timeout 为 3000毫秒(视情况而写,有时设500 ms)。一旦请求发送,取得数据后将要调用onResult方法。
[Test(async,timeout="3000")]
public function testServiceRequest():void
{
service.url = "assets/file.xml";
service.addEventListener(ResultEvent.RESULT,
Async.asyncHandler(this, onResult, 500 ), false, 0, true );
service.send();
}
testServiceRequest的result 处理方法,是断言event含有 result 属性:
private function onResult(event:ResultEvent, passThroughData:Object):void
{
Assert.assertTrue( event.hasOwnProperty("result") );
}
同样的道理,若向一个不存的url请求数据里,必然会返回falut信息。所以在testFailedServiceRequest()中,就是指向了一个不存在的url,侦听它的FaultEvent事件,在fault事件处理者中,断言FaultEvent的对象event含有faultCode属性。
testEvent()方法教我们如何进行自定义事件的异步测试,自定义一个事件类型为:
"eventType";然后在侦听方法中,断言事件类型为自定义的类型。
private function handleAsyncEvnet(event:Event, passThroughData:Object):void
{
Assert.assertEquals( event.type, "eventType" );
}
将创建的Test类只需加入FlexUnit4AppSuite中就可以运行test了。
AS3是基于事件驱动模式的语言,所以在开发的过程中,你将要test许多的case是关于异步测试的。FlexUnit4 为我们提供了基于标签级别的、便于写测试代码。
推测
FlexUnit4 引入了一个全新的概念就是推测。推测,其实是建议的意思。允许你创建一个test对检查你的假设,即关于一个test应具有的行为。 你要测试一个具有很大的或很多数值的方法时,这种类型的测试是很有用的。 这样的测试用到参数(数据点),并且这些数据点在整个test中是结合使用的。
创建一个新的测试套件,名为FlexUnit4TheorySuite。
package flexUnitTests
{
import org.flexunit.assertThat;
import org.flexunit.assumeThat;
import org.flexunit.experimental.theories.Theories;
import org.hamcrest.number.greaterThan;
import org.hamcrest.object.instanceOf;
[Suite]
[RunWith("org.flexunit.experimental.theories.Theories")]
public class FlexUnit4TheorySuite
{
private var theory:Theories;
//-----------------------------------------------------------------------
//
// DataPoints
// //-----------------------------------------------------------------------
[DataPoint]
public static var number:Number = 5;
//-----------------------------------------------------------------------
//
// Theories
// //-----------------------------------------------------------------------
[Theory]
public function testNumber( number:Number ):void
{
assumeThat( number, greaterThan( 0 ) );
assertThat( number, instanceOf(Number) );
}
}
}
RunWith标签表明一个runner 实现的是另外一个接口,不再是默认的接口。
[Suite]
[RunWith("org.flexunit.experimental.theories.Theories")]
我们设置用number参数作为一个数据点。
[DataPoint]
public static var number:Number = 5;
接下来,我们设置一个推测(theory)用来检查。我们将要检测number是大于5,并且是一个Number类型的。
[Theory]
public function testNumber( number:Number ):void
{
assumeThat( number, greaterThan( 0 ) );
assertThat( number, instanceOf(Number) );
}
将这个套件添加到FlexUnitApplication.mxml 的currentRunTestSuite()方法中的testsToRun数组中。
testsToRun.push(flexUnitTests.FlexUnit4TheorySuite);
测试界面
Flex 用于构建图形用户界面,包括外观和行为。在你的程序中,有时需要测试外观和行为。
FlexUnit 1中没有任何去测试用户界面的能力,MXML组件也不能供单位测试所选择。而FlexUnit 4中有一个序列的概念,这样你可以创建一个序列来保存你所界面上所有要执行的操作。
例如,假设你测试用户在程序中点击按钮。来看下面的代码:
package flexUnitTests
{
import flash.events.Event;
import flash.events.MouseEvent;
import mx.controls.Button;
import mx.core.UIComponent;
import mx.events.FlexEvent;
import org.flexunit.asserts.assertEquals;
import org.flexunit.async.Async;
import org.fluint.sequence.SequenceRunner;
import org.fluint.sequence.SequenceSetter;
import org.fluint.sequence.SequenceWaiter;
import org.fluint.uiImpersonation.UIImpersonator;
public class FlexUnit4CheckUITester
{
private var component:UIComponent;
private var btn:Button;
//------------------------------
//
//Before and After
//
//------------------------------
[Before(async,ui)]
public function setUp():void
{
component = new UIComponent();
btn = new Button();
component.addChild(btn);
btn.addEventListener(MouseEvent.CLICK,function():void
{
component.dispatchEvent(new Event('myButtonClicked'));
});
Async.proceedOnEvent(this,component,FlexEvent.CREATION_COMPLETE,500);
UIImpersonator.addChild(component);
}
[After(async,ui)]
public function tearDown():void
{
UIImpersonator.removeChild(component);
component = null;
}
//------------------------------------------------------
//
// Tests
//
//-------------------------------------------------------
[Test(async,ui)]
public function testButtonClick():void
{
Async.handleEvent(this,component,"myButtonClicked",handleButtonClickEvent,500);
btn.dispatchEvent(new MouseEvent(MouseEvent.CLICK,true,false));
}
[Test(async,ui)]
public function testButtonClickSequence():void
{
var sequence: SequenceRunner = new SequenceRunner(this);
var passThroughData:Object = new Object();
passThroughData.buttonLabel = 'Click button';
with(sequence)
{
addStep(new SequenceSetter(btn,{label:passThroughData.buttonLabel}));
addStep(new SequenceWaiter(component,'myButtonClicked',500));
addAssertHandler(handleButtonClickSqEvent,passThroughData);
run();
}
btn.dispatchEvent(new MouseEvent(MouseEvent.CLICK,true,false));
}
//---------------------------------------------------------------
//
// Handlers
// //---------------------------------------------------------------
private function handleButtonClickEvent(event:Event,passThroughData:Object):void
{
assertEquals(event.type, "myButtonClicked");
trace("handleButtonClickEvent");
}
private function handleButtonClickSqEvent(event:* , passThroughData:Object):void
{
assertEquals(passThroughData.buttonLabel,btn.label);
trace("handleButtonClickSqEvent");
}
}
}
你创建了将要测试的对象,一个组件和按钮的实例。这仅是一个例子,但在真实的UI中,你要用实例的MXML组件。你可以按照上例中那样创建MXML组件的实例或application 对象。
private var component:UIComponent;
private var btn:Button;
Before标签有async,ui两个属性,标明你要等待一个异步的事件,比如本例中的FlexEvent.CREATION_COMPLETE 事件。一旦接受到该事件到,它将创建的组件添加到UIImpersonator 组件里。因为UIImpersonator 继承了Assert 类,并允许添加组件和测试组件。
在setUp()方法里,你添加了一个button并添加了该button的Click事件的侦听者。在本例中,侦听者方法里是component组件派发myButtonClicked事件。
[Before(async,ui)]
public function setUp():void
{
component = new UIComponent();
btn = new Button();
component.addChild(btn);
btn.addEventListener(MouseEvent.CLICK,function():void
{
component.dispatchEvent(new Event('myButtonClicked'));
});
Async.proceedOnEvent(this,component,FlexEvent.CREATION_COMPLETE,500);
UIImpersonator.addChild(component);
}
一旦你的测试结束,你将要把组件从UIImpersonator中移出,并设置该组件为null。
[After(async,ui)]
public function tearDown():void
{
UIImpersonator.removeChild(component);
component = null;
}
首先测试的是,你创建button点击事件。一旦button的MouseEvent.CLICK事件被派发,则component则会派发myButtonClicked事件。这时由于Async对象添加了对component的myButtonClicked事件的侦听,所以会调用handleButtonClickEvent方法。
[Test(async,ui)]
public function testButtonClick():void
{ Async.handleEvent(this,component,"myButtonClicked",handleButtonClickEvent,500);
btn.dispatchEvent(new MouseEvent(MouseEvent.CLICK,true,false));
}
接下来的测试是,你要创建一个序列。这个序列允许你模仿用户使用控件的情况。在本例中,我们设置按钮的label的属性,并点击该按钮。要注意的是,我们用了一个passThroughData对象存储 赋给btn的label的属性值。然后,再添加一个断言处理方法,并把要比较的对象作为参数。
[Test(async,ui)]
public function testButtonClickSequence():void
{
var sequence: SequenceRunner = new SequenceRunner(this);
var passThroughData:Object = new Object();
passThroughData.buttonLabel = 'Click button';
with(sequence)
{
addStep(new SequenceSetter(btn,{label:passThroughData.buttonLabel}));
addStep(new SequenceWaiter(component,'myButtonClicked',500));
addAssertHandler(handleButtonClickSqEvent,passThroughData);
run();
}
btn.dispatchEvent(new MouseEvent(MouseEvent.CLICK,true,false));
}
在handleButtonClickSqEvent事件处理方法中,检查按钮的label属性是否等于passThroughData存储的值。
private function handleButtonClickSqEvent(event:* , passThroughData:Object):void
{
assertEquals(passThroughData.buttonLabel,btn.label);
}
正如你看到先前的例子,你可以用FlexUnit4来测试可视化的组件,这样能够帮助你创建更好的用户界面。
利用FlexUnit4进行测试驱动开发
FlexUnit 和 TDD(测试驱动开发)是携手并进的。TDD是一个软件开发技术,我们可以利用FlexUnit4来实现这一技术。这个方法论起源于程序员在编写完代码后对代码进行一个测试。在1999年,极限编程(XP)讨论如何解决工程的需求经常变更时怎么开发的问题,提到了TDD在编码之前先写测试的方法。注意TDD并不是一个完整的开发周期,它只是极限编程的一个部分。在编写代码之前编写测试,这样会允许你在小范围的演示你的工作,而不是让客户等待所有完成后再展示给他们。
每次都是小增量地添加代码,这样在项目结束前给客户有足够的时间去变更需求;同时也能确保你的程序不会出错,哪些代码是必须的哪些是不再需要的。
重要的是用TDD技术产出良好的代码,而不是创建一个测试平台。代码具有可测性是另外的好处。
TDD依赖的概念就是创建的任何代码都要有可测性;如果你创建了一个不可测的代码,那么你就要再三思考它是否有必要创建。
极限编程利用TDD技术的概念创造出了每隔几周作为一个开发周期的迭代计划。迭代计划基于用户的需求和通过测试的必要代码。一旦测试完成,代码就要重构,删除多余的代码,创造更精简的代码。最后,迭代计划团队提供了有效的应用程序。
我们来讲解一下TDD技术,如图1-12所示:
1. 添加测试。首先要去理解业务逻辑需求,设想所有可能的场景。在某种情况下,需求不是很清楚,你可提出问题,而不是等于软件开发快要结束时再质疑需求,那里所需的成本就比较大了。
2. 编写失败的测试。这一阶段,必须确保测试单元本身是工作的,且不能通过;因为你还没有写任何代码。
3. 编写代码。在这一阶段,你编写最简单且高效地通过测试。这时不需要包含任何的设计模式,这些代码将来可能会被修改和清除。目前的目标只是通过测试。
4. 测试通过。一旦你编完代码,测试通过,并且测试符合所有业务需求;然后将结果与客户或同事讨论。
5. 重构。目前你的测试完成,并且满足了所有的业务需求,接下来要确保代码是作为产品的代码,所要替换不必须的临时变量,还有就是要添加设计模式,移除重复的代码,创建类等工作。
图1-12. 测试驱动开发流程图
在开始使用Flash Builder 4之前,按照以下过程:
打开 Flash Builder 4 。选择 File(文件) ➤ New(新建) ➤ Flex Project (Flex 工程),命名为FlexUnitExample ,然后点击OK。
现在你已经可以开始了,传统上讲,以开始之前有许多工作要做的,比如创建UML图表之类的。思考下面这个例子:你接到一个业务需求的任务,这个业务需求你以前没有接触过,你只能假设如何去做。然后由你的认为的需求而不是你真正的需求来驱动图表。TDD使所有的事件都颠倒过来了。你可以按照下面的步骤来开始的你的业务需求:
• 你需求一个帮助类来读取以XML文件格式的员工信息
• 一旦员工信息被读取,它将转换为值对象类
• 员工信息将呈现在屏幕上
•记住这些需求进行下去
创建测试套件和测试用例
你下一步就是创建测试套件和测试用例。
1. 点击刚创建的工程名,并选择File(文件) ➤ New(新建) ➤ Test Suite Class(测试套件类)
2. 在弹出的窗口中,设置套件名为GetEmployeesSuite,并点击Finish(完成)。
3. 接着,创建测试用例类File(文件)➤ New (新建)➤ Test Case Class (测试用例类)
虽然测试套件类的代码已生成,但并没有包含任何测试用例类。记得将你要测试的用例类在测试套件中声明一下,如下代码:
package flexUnitTests
{
[Suite]
[RunWith("org.flexunit.runners.Suite")]
public class GetEmployeesSuite
{
public var getEmployeesInfoTester:GetEmployeesInfoTester;
}
}
在应用程序的包结构中你可发现 GetEmployyeesInfoTester.as和GetEmployeesInfoSuite.as和flexUnitCompilerApplication.mxml文件(如图 1-13)
图1-13 Flash Builder 4 包浏览
写失败用例
使用TDD的下一步就是编写失败的测试类。不像传统的编程,你编写测试之前先写代码。这是通过类的名称预先想一下你将要使用的方法,及这些方法需要完成什么任务。在某种情况下,假设你有一个员工的列表,你想添加一个新的员工。将创建的测试是创建实用程序类的实例及是后来创建的:GetEmployyeesInfo。另外,你使用[Before]设置类的实例的同时,也要将该实例设置成null 在[After]方法中,这样做避免了内存的泄漏。
面向对象的编程基于的原则是每个方法都有一个目的;目标是创建一个测试用来断言该方法的目的是否正确。
testAddItem()使用辅助类来添加一项,并检查集合中的第一项是否是刚添加的那一项。一旦你编译程序,将会在编译时出现下面图1-15的错误信息。这其实是件好事情,编译器告诉你下一步该做什么。
图1-15 FlexUnitTest 显示的编译错误
编写代码
你从编译器里获得的下面的错误信息,只要把这些错误解决了才能运行程序:
• 无法找到GetEmployeesInfo类型
• 调用一个可能未定义的GetEmployeesInfo方法
首先代码丢失了辅助类。创建GetEmployeesInfo类,并且将它放在新建的utils包下。选择 文件(File) ➤新建包 (New Package)(见图1-16)。
图1-16 建新包的向导
接下来创建GetEmployeesInfo类。 选择 文件(File) ➤新建ActionScript类 (New ActionScript class)
设置名称为GetEmployeesInfo并选择父类为 flash.event.EventDispatcher 类。选择完成(见图1-17)。
图1-17 新建一个ActionScript类向导
package utils
{
import flash.events.EventDispatcher;
import flash.events.IEventDispatcher;
import mx.collections.ArrayCollection;
import mx.rpc.http.HTTPService;
public class GetEmployeesInfo extends EventDispatcher
{
private var service:HTTPService;
private var _employeesCollection:ArrayCollection;
public function GetEmployeesInfo()
{
_employeesCollection = new ArrayCollection();
}
public function get employeesCollection():ArrayCollection
{
return _employeesCollection;
}
public function addItem(name:String,phone:String,age:String,email:String):void
{
var item:Object = {name: name;phone:phone,age:age,email:email};
employeesCollection.addItem(item);
}
}
}
testAddItem方法实际是将一个对象添加到一个集合中,所以你可容易地测试调用它的方法并传入一个员工信息,接着检查集合中新添加的是否正确。
public function testAddItem():void
{
classToTestRef.addItem("John Do","212-222-2222","25","john.do@gamil.com");
assertEquals(classToTestRef.employeesCollection.getItemAt(0).name,"John Do");
}
再次编译程序,则编译器的错误信息将消失。
测试通过
运行FlexUnit 测试,Flash builder 4在启动和调用图标下添加了一个菜单叫FlexUnit Tests。如图 1-18。
图1-18 执行 FlexUnit Tests 插件
在接下来的窗口中,你要选择测试套件或测试用例进行运行。在目前的情况下,我们只有一个方法要测试。见图1-19。
图1-19 运行FlexUnit Test 配置窗口
注意:选择Test Suite 或 Test Cases 其中的一个,而不是两个都选;否则会测试两次。
在编译完成后,浏览器打开并显示结果(见图1-20)。
• 共1个测试进行了运行.
• 1 个成功.
• 0 个失败.
• 0 个错误.
• 0 个忽略.
图1-20 FlexUnit Test 显示在浏览器中的结果
一旦你关闭浏览器,你可以在IDE的FlexUnit 结果栏中看到结果(如图1-21)。从视图中你看到测试通过并且是绿灯。
图 1-21 FlexUnit 结果视图栏
一旦你写完所有的代码并测试通过,也就是说你的测试满足了需求文档的业务需求;所以此时,你可以将你的成果分享给客户或团队的其它成员。
重构代码
目前你的测试通过,你可以重构代码为了将来做成产品做准备。比如,你添加一个设计模式来代替一块if...else 语句。在目前的情况下,是不需要重构的,因方代码太少太简单。
如果需要重复和清洗
你可以继续为Service 调用和检索员工信息数据的XML文件创建单元测试,然后将所有的信息添加上一个list上,最后当完成时派发一个事件。
为检索员工信息编写失败的测试
你可以继续为服务的调用检索员工的信息写一个新的测试用例,或者在原有测试用例的基础上添加测试方法。在目前的情况下,你可以自定义一个事件用来传递员工的信息。看下面的测试方法:
[Test]
public function testLoad():void
{
classToTestRef.addEventListener(RetrieveInformationEvent.RETRIVE_INFORMATION,addAsync(onResult,500));
classToTestRef.load("assets/file.xml");
}
[Test]
public function onResult(event:RetrieveInformationEvent):void
{
assertNotNull(event.employeesCollection);
}
testLoad()方法添加了一个事件的侦听,所以当异常调用完成后就会调用onResult方法来处理结果信息。注意你现在使用addAsync函数,它表示你要等待500毫秒来完成调用。
onResult()方法检查确保你取得的结果。在这个测试中你不在乎结果是什么类型的,只是将取得的结果添加上集合中去。你可以创建另外一个测试,用来检查数据的完整性。
写检索员工信息的代码
下面是GetEmployeesInfoTester.as 类完整的代码。
package flexUnitTests
{
import org.flexunit.asserts.assertEquals;
import org.flexunit.asserts.assertNotNull;
import org.flexunit.async.Async;
import utils.GetEmployeesInfo;
public class GetEmployeesInfoTester
{
//引用 类
public var classToTestRef:GetEmployeesInfo;
[Before]
public function setUpBeforeClass():void
{
classToTestRef = new GetEmployeesInfo();
}
[After]
public function tearDownAfterClass():void
{
classToTestRef = null;
}
[Test]
public function testAddItem():void
{
classToTestRef.addItem("John Do","212-222-2222","25","john.do@gamil.com");
assertEquals(classToTestRef.employeesCollection.getItemAt(0).name,"John Do");
}
[Test]
public function testLoad():void
{
classToTestRef.addEventListener(RetrieveInformationEvent.RETRIVE_INFORMATION,
Async.asyncHandler(this,onResult,500),false,0,true);
classToTestRef.load("assets/file.xml");
}
private function onResult(event:
RetrieveInformationEvent):void
{
assertNotNull(event.employeesCollection);
}
}
}
编译该类则会得到编译时的错误信息。同样,这些错误信息提示你下一步要做什么。回到GetEmployeesInfo.as 类 添加一个加载方法,用来加载XML。下面是该类的完全代码:
package utils
{
import flash.events.EventDispatcher;
import flash.events.IEventDispatcher;
import mx.collections.ArrayCollection;
import mx.rpc.events.FaultEvent;
import mx.rpc.events.ResultEvent;
import mx.rpc.http.HTTPService;
import utils.events.RetrieveInformationEvent;
public class GetEmployeesInfo extends EventDispatcher
{
private var service:HTTPService;
private var _employeesCollection:ArrayCollection;
public function GetEmployeesInfo()
{
_employeesCollection = new ArrayCollection();
}
public function get employeesCollection():ArrayCollection
{
return _employeesCollection;
}
public function load(file:String):void
{
service = new HTTPService();
service.url = file;
service.resultFormat = "e4x";
service.addEventListener(ResultEvent.RESULT,onResult);
service.addEventListener(FaultEvent.FAULT,onFault);
service.send();
}
private function onResult(event:ResultEvent):void
{
var employees:XML = new XML(event.result);
var employee:XML;
for each(employee in employees.employee)
{
this.addItem(employee.name,employee.phone,employee.age,employee.email);
}
this.dispatchEvent(new RetrieveInformationEvent(employeesCollection));
}
private function onFault(event:FaultEvent):void
{
trace("errors loading file");
}
public function addItem(name:String,phone:String,age:String,email:String):void
{
var item:Object = {name: name,phone:phone,age:age,email:email};
employeesCollection.addItem(item);
}
}
}
有一个变量用来存放HTTPService的实例,并用它来进行服务的调用。
private var service:HTTPService;
有一个集合变量用来存储结果。不允许子类直接改变结果集,但仍然有一个可以对集合进行赋值的地方,就是构造函数。
private var _employeesCollection:ArrayCollection;
public function GetEmployeesInfo()
{
_employeesCollection = new ArrayCollection();
}
load()方法用来指向将加载的文件,添加事件侦听并开始服务调用。
public function load(file:String):void
{
service = new HTTPService();
service.url = file;
service.resultFormat = "e4x";
service.addEventListener(ResultEvent.RESULT,onResult);
service.addEventListener(FaultEvent.FAULT,onFault);
service.send();
}
一旦服务调用成功就会调用onResult()方法。遍历整个结果并通过addItem()方法将员工信息添加到集合中去。一旦这个过程完成,将使用dispatchEvent()方法派发一个RetrieveInformationEvent事件并携带employeesCollection集合信息。
private function onResult(event:ResultEvent):void
{
var employees:XML = new XML(event.result);
var employee:XML;
for each(employee in employees.employee)
{
this.addItem(employee.name,employee.phone,employee.age,employee.email);
}
this.dispatchEvent(new RetrieveInformationEvent(employeesCollection));
}
当服务调用失败时,将会调用onFault()方法。比如访问一个不存在的文件或安全沙箱问题。
private function onFault(event:FaultEvent):void
{
trace("errors loading file");
}
在GetEmployeesInfoTester.as类中的testLoad()方法中,
[Test(async, timeout="1000")]
public function testLoad():void
{
classToTestRef.addEventListener(RetrieveInformationEvent.RETRIVE_INFORMATION,
Async.asyncHandler(this,onResult,500),false,0,true);
classToTestRef.load("assets/file.xml");
}
classToTestRef 添加了RetrieveInformationEvent..RETRIVE_INFORMATION事件的侦听。因为classToTestRef 是GetEmployeesInfo 类的对象,所以要执行 GetEmployeesInfo 的 load()方法。
创建一个XML文件,存放在assets包下,即为assets/file.xml 其中的内容可以如下:
<?xml version="1.0" encoding="utf-8"?>
<employees>
<employee>
<name>John Do</name>
<phone>212-222-2222</phone>
<age>20</age>
<email>john@youremail.com</email>
</employee>
<employee>
<name>Jane Smith</name>
<phone>212-333-3333</phone>
<age>21</age>
<email>jane@youremail.com</email>
</employee>
</employees>
最后,一旦 RetrieveInformationEvent 事件被派发,将要执行GetEmployeesInfoTester 类 onInfoRetrieved
private function onInfoRetrieved(event:RetrieveInformationEvent,
passThroughData:Object):void
{
trace(event.employeesCollection.getItemAt(0).name);
}
创建一个自定义事件类RetrieveInformationEvent,该事件存放所有员工的信息。
package utils.events
{
import flash.events.Event;
import mx.collections.ArrayCollection;
public class RetrieveInformationEvent extends Event
{
public static const RETRIVE_INFORMATION:String = "RetrieveInformationEvent";
public var employeesCollection:ArrayCollection;
public function RetrieveInformationEvent(employeesCollection:ArrayCollection)
{
this.employeesCollection = employeesCollection;
super(RETRIVE_INFORMATION, false, false);
}
}
}
编译并运行FlexUnit Test,你会在控制台窗口中看到trace语句的结果。
测试通过
现在测试两个方法,一个是testAddItem()方法,另一个是testLoad()方法。而这次的重点的测试testLoad()方法,通过HTTPService访问文件,把数据检索过来。运行FlexUnit Test,会测试通过,先到绿灯的显示,如图1-22。
图1-22 执行FlexUnit Test
重构
唯一可重构的代码就是,添加一元数据使它指向RetrieveInformationEvent事件。所以在GetEmployeesInfo 类开头部分应添加下面的代码:
[Event(name="retriveInformation", type="utils.events.RetrieveInformationEvent")]
小结
在这一章中,我们主要是讲解了FlexUnit 4和测试驱动开发模式(TDD)。一开始先总述了FlexUnit 4,然后是如何创建测试套件,测试用例,和 测试runner类。还讲述了FlexUnit4 中所有断言的方法,异步测试和异常处理。另外还讲述了FlexUnit 4额外的断言方法,如:Hamcrest,推测,测试,测试用户界面。
在本章中的第二部分,我们讨论了使用FlexUnit4测试驱动开发模式。给你展示如何写失败的测试用例,编写代码,测试通过,及重构你的代码。我们期望你使用TTD开发移动设备,Web程序,桌面程序,写出更好、更易维护、更复用的代码。
注:PDF格式的文档及source codes, 整理完成后上传在CSND的资源上。