24.8.1. 问题
我需要测试可视化组件
24.8.2. 解决办法
展示将组件放在可视体系中然后测试它。
24.8.3. 讨论
有人认为可视组件的测试已偏离了单元测试的目的,因为它们很难被独立出来进行测试,以便能控制测试条件。测试功能丰富的Flex框架组件是很复杂的,比如怎样确定某个方法是否被正确调用。样式和父容器也会影响一个组件的行为。因此,你最好是用自动化功能测试的方法来测试可视组件。
在测试一个可视组件之前,该组件必须通过了各种生命周期步骤。当组件被添加到显示体系后Flex框架会自动进行处理。TestCases虽然不是一个可视组件,这意味着组件必须与外部的TestCase相关联。这种外部关联意味着无论测试失败还是成功你都必须小心清理善后工作;否则可能会影响到其他组件的测试。
24.8.3.1. 组件测试模式
获得显示对象引用最简单的方法是使用Application.application。因为TestCase是运行Flex应用程序之上,它是一个单例实例。可视组件的创建和激活并不是一个同步的行为;在可被测试之前, 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实例,在某个已知时间内播放)。基于时间的测试最主要的缺点就是如果测试环境的速度和资源发生改变的话可能会导致误报。等待一个固定时间也意味着整个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);