24.8.1. 問題
我需要測試可視化組件
24.8.2. 解決辦法
展示將組件放在可視體系中然后測試它。
24.8.3. 討論
有人認為可視組件的測試已偏離了單元測試的目的,因為它們很難被獨立出來進行測試,以便能控制測試條件。測試功能豐富的Flex框架組件是很復雜的,比如怎樣確定某個方法是否被正確調用。樣式和父容器也會影響一個組件的行為。因此,你最好是用自動化功能測試的方法來測試可視組件。
在測試一個可視組件之前,該組件必須通過了各種生命周期步驟。當組件被添加到顯示體系后Flex框架會自動進行處理。TestCases雖然不是一個可視組件,這意味著組件必須與外部的TestCase相關聯。這種外部關聯意味著無論測試失敗還是成功你都必須小心清理善后工作;否則可能會影響到其他組件的測試。
24.8.3.1. 組件測試模式
獲得顯示對象引用最簡單的方法是使用Application.application。因為TestCase是運行Flex應用程序之上,它是一個單例實例??梢暯M件的創建和激活并不是一個同步的行為;在可被測試之前, TestCase 需要等待組件進入一個已知的狀態。通過使用addAsync 等待FlexEvent.CREATION_COMPLETE事件是最簡單的方法知道新創建的組件已進入已知狀態。要確保一個TestCase方法不會影響到其他正在運行的TestCase方法,被創建的組件在被移除前必須清理和釋放任何外部引用。使用tearDown方法和類實例變量是完成這兩個任務的最好方法。下面的例子代碼演示Tile組件的創建,連接,激活和清理:+展開-ActionScript
package mx.containers
{
import flexunit.framework.TestCase;
import mx.core.Application;
import mx.events.FlexEvent;
public class TileTest extends TestCase
{
// class variable allows tearDown() to access the instance
private var _tile:Tile;
override public function tearDown():void
{
try
{
Application.application.removeChild(_tile);
}
catch (argumentError:ArgumentError)
{
// safe to ignore, just means component was never added
}
_tile = null;
}
public function testTile():void
{
_tile = new Tile();
_tile.addEventListener(FlexEvent.CREATION_COMPLETE,addAsync(verifyTile, 1000));
Application.application.addChild(_tile);
}
private function verifyTile(flexEvent:FlexEvent):void{
// component now ready for testing
assertTrue(_tile.initialized);
}
}
}
這里需要注意的關鍵點是定義了一個類變量,允許tearDown方法引用這個被創建和添加到Application.application的實例對象。另外組件被添加到Application.application可能還沒有成功,這就是為什么tearDown方法中的removeChild調用被放置在try...catch塊中以防止拋出任何異常。測試方法使用addAsync在運行測試之前進行等待,直到組件進入一個穩定的狀態。
24.8.3.2. 組件創建測試
雖然你可以手動調用測試和各種其他Flex框架組件方法,測試將更好的模擬對象所運行的環境。不像單元測試,組件所連接外部環境將不能被嚴格控制,這意味著你必須集中于組件的測試而不能顧及周圍環境。例如,早先創建的Tile容器的布局邏輯可被測試:+展開-ActionScript
public function testTileLayout():void
{
_tile = new Tile();
var canvas:Canvas = new Canvas();
canvas.width = 100;
canvas.height = 100;
_tile.addChild(canvas);
canvas = new Canvas();
canvas.width = 50;
canvas.height = 50;
_tile.addChild(canvas);
canvas = new Canvas();
canvas.width = 150;
canvas.height = 50;
_tile.addChild(canvas);
_tile.addEventListener(FlexEvent.CREATION_COMPLETE,addAsync(verifyTileLayout, 1000));
Application.application.addChild(_tile);
}
private function verifyTileLayout(flexEvent:FlexEvent):void
{
var horizontalGap:int =int(_tile.getStyle("horizontalGap"));
var verticalGap:int =int(_tile.getStyle("verticalGap"));
assertEquals(300 + horizontalGap, _tile.width);
assertEquals(200 + verticalGap, _tile.height);
assertEquals(3, _tile.numChildren);
assertEquals(0, _tile.getChildAt(0).x);
assertEquals(0, _tile.getChildAt(0).y);
assertEquals(150 + horizontalGap,
_tile.getChildAt(1).x);
assertEquals(0, _tile.getChildAt(1).y);
assertEquals(0, _tile.getChildAt(2).x);
assertEquals(100 + verticalGap, _tile.getChildAt(2).y);
}
這個例子中,三個大小不同的子組件被添加到Tile。根據Tile布局邏輯,這個例子應該創建一個2 x 2 的網格并使每個網格的寬度和高度最大化以容納子組件。Verify方法斷言默認邏輯將會生產的結果。重要的是要注意到測試只注重于組件所使用到的邏輯。這不是測試布局是否看起來足夠好,而只是其行為與文檔相匹配。另外重要的一點需要注意,就是關于當前層級的組件測試會影響到在其之上的組件樣式。這個測試方法在它創建實例時可以設置樣式值以便確認該值是否被使用過。
24.8.3.3. Postcreation 測試
組件創建后,額外的變化會讓測試變得很困難。通常使用FlexEvent.UPDATE_COMPLETE事件,但組件的一個簡單變化會多次觸發此事件。雖然可以建立邏輯以正確處理這多個事件,但是TestCase除了測試組件內邏輯并不會區分Flex框架事件和UI更新邏輯。因此集中于組件邏輯的測試設計確實是一門藝術。這就是為什么要在這個級別進行組件測試而不是單元測試。
下面的例子添加了另外的子組件到先前創建的Tile,檢測發生的變化:+展開-ActionScript
// class variable to track the last addAsync() Function instance
private var _async:Function;
public function testTileLayoutChangeAfterCreate():void
{
_tile = new Tile();
var canvas:Canvas = new Canvas();
canvas.width = 100;
canvas.height = 100;
_tile.addChild(canvas);
canvas = new Canvas();
canvas.width = 50;
canvas.height = 50;
_tile.addChild(canvas);
canvas = new Canvas();
canvas.width = 150;
canvas.height = 50;
_tile.addChild(canvas);
_tile.addEventListener(FlexEvent.CREATION_COMPLETE,addAsync(verifyTileLayoutAfterCreate, 1000));
Application.application.addChild(_tile);
}
private function
verifyTileLayoutAfterCreate(flexEvent:FlexEvent):void
{
var horizontalGap:int =int(_tile.getStyle("horizontalGap"));
var verticalGap:int =int(_tile.getStyle("verticalGap"));
assertEquals(300 + horizontalGap, _tile.width);
assertEquals(200 + verticalGap, _tile.height);
assertEquals(3, _tile.numChildren);
assertEquals(0, _tile.getChildAt(0).x);
assertEquals(0, _tile.getChildAt(0).y);
assertEquals(150 + horizontalGap,
_tile.getChildAt(1).x);
assertEquals(0, _tile.getChildAt(1).y);
assertEquals(0, _tile.getChildAt(2).x);
assertEquals(100 + verticalGap, _tile.getChildAt(2).y);
var canvas:Canvas = new Canvas();
canvas.width = 200;
canvas.height = 100;
_tile.addChild(canvas);
_async = addAsync(verifyTileLayoutChanging, 1000);
_tile.addEventListener(FlexEvent.UPDATE_COMPLETE,
_async);
}
private function
verifyTileLayoutChanging(flexEvent:FlexEvent):void
{
_tile.removeEventListener(FlexEvent.UPDATE_COMPLETE,
_async);
_tile.addEventListener(FlexEvent.UPDATE_COMPLETE,
addAsync
(verifyTileLayoutChangeAfterCreate, 1000));
}
private function verifyTileLayoutChangeAfterCreate(flexEvent:FlexEvent):void{
var horizontalGap:int =int(_tile.getStyle("horizontalGap"));
var verticalGap:int =int(_tile.getStyle("verticalGap"));
assertEquals(400 + horizontalGap, _tile.width);
assertEquals(200 + verticalGap, _tile.height);
assertEquals(4, _tile.numChildren);
assertEquals(0, _tile.getChildAt(0).x);
assertEquals(0, _tile.getChildAt(0).y);
assertEquals(200 + horizontalGap,
_tile.getChildAt(1).x);
assertEquals(0, _tile.getChildAt(1).y);
assertEquals(0, _tile.getChildAt(2).x);
assertEquals(100 + verticalGap, _tile.getChildAt(2).y);
assertEquals(200 + horizontalGap,
_tile.getChildAt(3).x);
assertEquals(100 + verticalGap, _tile.getChildAt(3).y);
}
事件處理邏輯現在使用一個類變量來跟蹤最后通過addAsync添加的異步函數,這是為了允許改監聽器可被移除并添加一個不同的監聽器來處理第二次觸發的同類事件。如果這時另一個變化發生,將會觸發另一個FlexEvent.UPDATE_COMPLETE,verifyTileLayoutChanging方法也必須存儲它的addAsync函數為了它能被移除。如果Flex框架邏輯改變了如何觸發事件,那這一鏈式事件處理就顯得很脆弱了,整個代碼測試將會導致失敗。這個測試沒有處理這兩個觸發的FlexEvent.UPDATE_COMPLETE事件為了組件能順利完成子組件的布局任務;在這個級別試圖捕捉組件邏輯會產生意想不到的效果。如果在中間狀態verifyTileLayoutChanging中捕捉組件邏輯,在這個方法中的斷言將發揮作用,如果事件沒有被正確觸發,這些變化的事件將會保證此測試失敗。
雖然組件也會觸發額外的事件,如Event.RESIZE,但是該事件所在的組件狀態通常是不穩定的。正如處在Event.RESIZE的Tile那樣,組件的寬度發生變化,但是其子組件的位置卻還沒有。另外還可能有這樣的排隊操作,當要移除顯示層級中的組件時操作隊列中卻要試圖訪問該組件,這將會導致錯誤。當測試那些采用同步方式更新邏輯的組件,移除其他監聽器還需要的組件時要盡量避免發生這些問題。換句話說,被測試組件發出的事件要清晰表明組件的變化已完全實現。不管你選擇什么方法處理這種情況,請記住有哪些方法是可靠的,有哪些測試行為是脫離組件的。
24.8.3.4. 根據時間測試
如果組件一下子產生了很多復雜的變化,維持事件的數量和順序將會非常困難。除了等待特定的事件外,另一個辦法就是去等待一段時間。這個方法可以輕松的處理多個被更新的對象或組件(使用Effect實例,在某個已知時間內播放)?;跁r間的測試最主要的缺點就是如果測試環境的速度和資源發生改變的話可能會導致誤報。等待一個固定時間也意味著整個TestSuite的所花時間將比添加異步或事件驅動的測試多出不少。
下面的代碼是把之前的Tile例子用基于時間的觸發器改寫一邊:+展開-ActionScript
private function waitToTest(listener:Function,waitTime:int):void
{
var timer:Timer = new Timer(waitTime, 1);
timer.addEventListener(TimerEvent.TIMER_COMPLETE,addAsync(listener,waitTime + 250));
timer.start();
}
public function testTileLayoutWithTimer():void
{
_tile = new Tile();
var canvas:Canvas = new Canvas();
canvas.width = 100;
canvas.height = 100;
_tile.addChild(canvas);
canvas = new Canvas();
canvas.width = 50;
canvas.height = 50;
_tile.addChild(canvas);
canvas = new Canvas();
canvas.width = 150;
canvas.height = 50;
_tile.addChild(canvas);
Application.application.addChild(_tile);
waitToTest(verifyTileLayoutCreateWithTimer, 500);
}
private function verifyTileLayoutCreateWithTimer(timerEvent:TimerEvent):void{
var horizontalGap:int =int(_tile.getStyle("horizontalGap"));
var verticalGap:int =int(_tile.getStyle("verticalGap"));
assertEquals(300 + horizontalGap, _tile.width);
assertEquals(200 + verticalGap, _tile.height);
assertEquals(3, _tile.numChildren);
assertEquals(0, _tile.getChildAt(0).x);
assertEquals(0, _tile.getChildAt(0).y);
assertEquals(150 + horizontalGap,
_tile.getChildAt(1).x);
assertEquals(0, _tile.getChildAt(1).y);
assertEquals(0, _tile.getChildAt(2).x);
assertEquals(100 + verticalGap, _tile.getChildAt(2).y);
var canvas:Canvas = new Canvas();
canvas.width = 200;
canvas.height = 100;
_tile.addChild(canvas);
waitToTest(verifyTileLayoutChangeWithTimer, 500);
}
private function verifyTileLayoutChangeWithTimer(timerEvent:TimerEvent):void{
var horizontalGap:int =int(_tile.getStyle("horizontalGap"));
var verticalGap:int =int(_tile.getStyle("verticalGap"));
assertEquals(400 + horizontalGap, _tile.width);
assertEquals(200 + verticalGap, _tile.height);
assertEquals(4, _tile.numChildren);
assertEquals(0, _tile.getChildAt(0).x);
assertEquals(0, _tile.getChildAt(0).y);
assertEquals(200 + horizontalGap,
_tile.getChildAt(1).x);
assertEquals(0, _tile.getChildAt(1).y);
assertEquals(0, _tile.getChildAt(2).x);
assertEquals(100 + verticalGap, _tile.getChildAt(2).y);
assertEquals(200 + horizontalGap,
_tile.getChildAt(3).x);
assertEquals(100 + verticalGap, _tile.getChildAt(3).y);
}
和之前的測試例子,能快速響應觸發的事件不同,這個版本的測試將最少要1秒時間,更多的時間被用在定時器延時上, 在這期間調用addAsync 處理。之前例子中包裝FlexEvent.UPDATE_COMPLETE監聽器的中間方法被移除了,但在其他測試代碼中保持一致。
24.8.3.5. 使用程序化的視覺斷言
獲取渲染組件的原始位圖數據能力能很方便的以編程方式來驗證可視組件的某個方面。這里有個例子將測試組件的背景和邊框樣式是如何改變的。創建組件實例后,可以捕捉其位圖數據并進行檢查。下面的例子通過添加Canvas邊框來測試能產生預期效果:+展開-ActionScript
package mx.containers
{
import flash.display.BitmapData;
import flexunit.framework.TestCase;
import mx.core.Application;
import mx.events.FlexEvent;
public class CanvasTest extends TestCase
{
// class variable allows tearDown() to access the instance
private var _canvas:Canvas;
override public function tearDown():void
{
try
{
Application.application.removeChild(_canvas);
}
catch (argumentError:ArgumentError)
{
// safe to ignore, just means component was never added
}
_canvas = null;
}
private function captureBitmapData():BitmapData
{
var bitmapData:BitmapData = new BitmapData(_canvas.width, _canvas.height);
bitmapData.draw(_canvas);
return bitmapData;
}
public function testBackgroundColor():void
{
_canvas = new Canvas();
_canvas.width = 10;
_canvas.height = 10;
_canvas.setStyle("backgroundColor", 0xFF0000);
_canvas.addEventListener(FlexEvent.CREATION_COMPLETE,addAsync(verifyBackgroundColor, 1000));
Application.application.addChild(_canvas);
}
private function
verifyBackgroundColor(flexEvent:FlexEvent):void
{
var bitmapData:BitmapData = captureBitmapData();
for (var x:int = 0; x < bitmapData.width; x++)
{
for (var y:int = 0; y < bitmapData.height; y++)
{
assertEquals("Pixel (" + x + ", " + y + ")",0xFF0000, bitmapData. getPixel(x, y));
}
}
}
public function testBorder():void
{
_canvas = new Canvas();
_canvas.width = 10;
_canvas.height = 10;
_canvas.setStyle("backgroundColor", 0xFF0000);
_canvas.setStyle("borderColor", 0x00FF00);
_canvas.setStyle("borderStyle", "solid");
_canvas.setStyle("borderThickness", 1);
_canvas.addEventListener(FlexEvent.CREATION_COMPLETE,
addAsync(verifyBorder, 1000));
Application.application.addChild(_canvas);
}
private function verifyBorder(flexEvent:FlexEvent):void
{
var bitmapData:BitmapData = captureBitmapData();
for (var x:int = 0; x < bitmapData.width; x++)
{
for (var y:int = 0; y < bitmapData.height; y++)
{
if ((x == 0) || (y == 0) || (x ==
bitmapData.width - 1) || (y == bitmapData.height - 1))
{
assertEquals("Pixel (" + x + ", " + y + ")",
0x00FF00,bitmapData.getPixel(x, y));
}
else
{
assertEquals("Pixel (" + x + ", " + y + ")",0xFF0000,bitmapData.getPixel(x, y));
}
}
}
}
}
}
testBackgroundColor方法驗證已設置背景顏色的Canvas的所有像素。testBorder方法驗證何時邊框被添加到Canvas,外邊框的像素值轉換為邊框顏色而其他所以像素仍保持背景色。捕獲位圖數據是在captureBitmapData方法中進行,使用它可以繪制任何Flex組件到BitmapData實例上。這是一項很強大的技術,可以用來驗證程序化的皮膚或其他很難進行單元測試的可視組件。
還有另一種方法測試組件的外觀。請到http://code.google.com/p/visualflexunit/看看Visual FlexUnit。
24.8.3.6. 隱藏被測試組件
添加測試組件到Application.application的一個副作用就是它將被渲染處理。這將導致當測試正在運行,組件正在被添加和移除時FlexUnit測試很難進行調整和重定位。要排除這個情況,你可以通過設置組件的visible和includeInLayout屬性為false來隱藏被測試組件。例如,如果要隱藏之前代碼中的Canvas的話,添加下面的代碼:+展開-ActionScript
_canvas.visible = false;
_canvas.includeInLayout = false;
Application.application.addChild(_canvas);