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的資源上。

 

 

 

 

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