AdvancED Flex 4 (一):使用測試驅動開發模式建立應用程式
Author: Shashank Tiwari & Elad Elrom
Translator: 李學錕
Chapter 1: 使用測試驅動開發模式建立應用程式....................................................... 1
FlexUnit 4 概述.........................................................................................................................................2
編寫第一個套件
編寫第一個測試用例類 ..............................................................................................................................5
檢查結果 ...................................................................................................................................................7
使用FlexUnit4進行測試驅動開發模式 .....................................................................................................25
小結..........................................................................................................................................................42
在Flex 4 中,有一個焦點的技術就是引入了應用程式的設計中心的開發,它主要是將表示層和邏輯層區別出來,並將表示層的工作交給使用
但是,隨著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;
publicclass FlexUnitTester
{
//--------------------------------------------------------------------------
//
// Before and After
//
//--------------------------------------------------------------------------
[Before]
publicfunction runBeforeEveryTest():void
{
// implement
}
[After]
publicfunction runAfterEveryTest():void
{
// implement
}
//--------------------------------------------------------------------------
//
// Tests
//
//--------------------------------------------------------------------------
[Test]
publicfunction checkMethod():void
{
Assert.assertTrue( true );
}
[Test(expected="RangeError")]
publicfunction rangeCheck():void
{
var child:Sprite = new Sprite();
child.getChildAt(0);
}
[Test(expected="flexunit.framework.AssertionFailedError")]
publicfunction testAssertNullNotEqualsNull():void
{
Assert.assertEquals( null, "" );
}
[Ignore("Not Ready to Run")]
[Test]
publicfunction 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]
publicfunction runBeforeEveryTest():void
{
// implement
}
[After]
publicfunction runAfterEveryTest():void
{
// implement
}
Test 標籤替換每個方法的字首,讓你有一種可以不和測試一塊啟動的辦法。
[Test]
publicfunction checkMethod():void
{
Assert.assertTrue( true );
}
在FlexUnit1中,你必須將不需要測試的方法給註釋掉。現在FlexUnit4只需要在該方法前新增Ignore 標籤就可跳過該方法運行了。
[Ignore("Not Ready to Run")]
[Test]
publicfunction 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]
publicfunction 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")]
publicfunction rangeCheck():void
{
var child:Sprite = new Sprite();
child.getChildAt(0);
}
另外一個例子期望是一個假設錯誤。來回顧一下testAsserNullNotEqualsNull方法,該方法期望出現AssertionFailedError 失敗的錯誤。assertEquals方法程式碼將是不通過的,因為null不等於"" ,所以假設是失敗的。
當表示式變為:Assert.assertEquals( null, null ); 然後你將得到成功的測試。
[Test(expected="flexunit.framework.AssertionFailedError")]
publicfunction 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;
publicfunction currentRunTestSuite():Array
{
var testsToRun:Array = new Array();
testsToRun.push(flexUnitTests.FlexUnit4AppSuite);
return testsToRun;
}
privatefunction 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()。 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;
publicclass FlexUnitCheckRangeTester
{
//--------------------------------------------------------------------------
//
// Before and After
//
//--------------------------------------------------------------------------
privatevar numbers:Array;
[Before]
publicfunction runBeforeEveryTest():void
{
numbers = [1, 2, 3, 4];
}
[After]
publicfunction runAfterEveryTest():void
{
numbers = null;
}
//--------------------------------------------------------------------------
//
// Tests
//
//--------------------------------------------------------------------------
[Test]
publicfunction shouldDemonstrateHamcrestInTests():void
{
assertThat(numbers, allOf(hasItem(equalTo(3)), hasItem(closeTo(5, 1))));
}
[Ingore]
[Test]
publicfunction 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;
publicclass AsynchronousTester
{
privatevar service:HTTPService;
//------------------------------------------------------------
//
// Before and After
////------------------------------------------------------------
[Before]
publicfunction runBeforeEveryTest():void
{
service = new HTTPService();
service.resultFormat = "e4x";
}
[After]
publicfunction runAfterEveryTest():void
{
service = null;
}
//------------------------------------------------------------
//
// Tests
////------------------------------------------------------------
[Test(async,timeout="3000")]
publicfunction 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")]
publicfunction 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")]
publicfunction 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")]
publicfunction testMultiAsync():void
{
testEvent();
testServiceRequest();
}
//------------------------------------------------------------
//
// Asynchronous handlers
////------------------------------------------------------------
privatefunction onResult(event:ResultEvent, passThroughData:Object):void
{
Assert.assertTrue( event.hasOwnProperty("result") );
}
privatefunction handleAsyncEvnet(event:Event, passThroughData:Object):void
{
Assert.assertEquals( event.type, "eventType" );
}
privatefunction 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")]
publicfunction 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 屬性:
privatefunction onResult(event:ResultEvent, passThroughData:Object):void
{
Assert.assertTrue( event.hasOwnProperty("result") );
}
同樣的道理,若向一個不存的url請求資料裡,必然會返回falut資訊。所以在testFailedServiceRequest()中,就是指向了一個不存在的url,偵聽它的FaultEvent事件,在fault事件處理者中,斷言FaultEvent的物件event含有faultCode屬性。
testEvent()方法教我們如何進行自定義事件的非同步測試,自定義一個事件型別為:
"eventType";然後在偵聽方法中,斷言事件型別為自定義的型別。
privatefunction 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")]
publicclass FlexUnit4TheorySuite
{
privatevar theory:Theories;
//-----------------------------------------------------------------------
//
// DataPoints
////-----------------------------------------------------------------------
[DataPoint]
publicstaticvar number:Number = 5;
//-----------------------------------------------------------------------
//
// Theories
////-----------------------------------------------------------------------
[Theory]
publicfunction testNumber( number:Number ):void
{
assumeThat( number, greaterThan( 0 ) );
assertThat( number, instanceOf(Number) );
}
}
}
RunWith標籤表明一個runner 實現的是另外一個介面,不再是預設的介面。
[Suite]
[RunWith("org.flexunit.experimental.theories.Theories")]
我們設定用number引數作為一個數據點。
[DataPoint]
publicstaticvar number:Number = 5;
接下來,我們設定一個推測