AdvancED Flex 4 (一):使用测试驱动开发模式创建应用程序

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在现在的应用程序上,即使程序使用了其它的框架如CairgormRobotlegs

TDD是可行的,即使是有质量保证(QA)部门的环境下,因为它提供了更稳固的代码供QA来创建他们需要的测试用例在用户界面上进行测试。

在本章中,将要讲解一些基本的内容及较高级的话题如:在现在程序中怎样开始使用TDD和如何为复杂的类创建单元测试,这样包括服务的调用及复杂的逻辑。

 

FlexUnit 4 概述

在冲入TDD之前,我们先来了解一下FlexUint 4,因为将要用到FlexUnit 4来创建测试。

让我们回顾一下它的成长历程。2003Adobe并购了一个咨询公司,后来成为了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 4runners的一个集合,它来运行创建一个测试的完整的设置。你可以定义每个runner来实现一个特定的接口。例如,你可以选择在运行测试时指向一个类来代替FlexUnit4默认创建的类。

 

 

 

这表明框架是足够灵活地支持将来的runners和允许开发者创建自己的runners,而且使用同一的UI。事实上,目前有FlexUnit 1,FlexUnit 4,FluintSLTrunner

 

 

编写你的第一个测试用例类

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,在IDEFlexUnit4 结果窗口中,看到了绿灯;代表Test全部正确。

FlexUnit4 是基于标签的,来看下面一些常用的标签:

• [Suite]: 表示该Class是一个套件类.

• [Test]: Test 标签替换测试方法的前缀 支持expectedasyncordertimeout, and ui 属性。 

• [RunWith]: 用于选择要使用的runner

• [Ignore]: 在方法前添加Ignore 标签来代替注释方法。

• [Before]: 替换FlexUnit1setup()方法,允许多个方法同时使用;支持asynctimeoutorder,ui 属性。

• [After]: 替换FlexUnit1teardown()方法,允许多个方法同时使用;支持asynctimeoutorder,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( nullnull ); 然后你将得到成功的测试。

[Test(expected="flexunit.framework.AssertionFailedError")]

public function testAssertNullNotEqualsNull():void

{

Assert.assertEquals( nullnull );

}

 

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();

}

 

testServiceRequestresult 处理方法,是断言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并添加了该buttonClick事件的侦听者。在本例中,侦听者方法里是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点击事件。一旦buttonMouseEvent.CLICK事件被派发,则component则会派发myButtonClicked事件。这时由于Async对象添加了对componentmyButtonClicked事件的侦听,所以会调用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对象存储 赋给btnlabel的属性值。然后,再添加一个断言处理方法,并把要比较的对象作为参数。

[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.asGetEmployeesInfoSuite.asflexUnitCompilerApplication.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","[email protected]");

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-20 FlexUnit Test 显示在浏览器中的结果

一旦你关闭浏览器,你可以在IDEFlexUnit 结果栏中看到结果(如图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","[email protected]");

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>[email protected]</email>

</employee>

<employee>

<name>Jane Smith</name>

<phone>212-333-3333</phone>

<age>21</age>

<email>[email protected]</email>

</employee>

</employees>

最后,一旦 RetrieveInformationEvent 事件被派发,将要执行GetEmployeesInfoTeste类 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, falsefalse);

}

}

}

编译并运行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的资源上。

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章