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","[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 個成功.
• 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","[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 事件被派發,將要執行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的資源上。