最近要用到Adobe Flex技术做开发,便重投Flex和AS的怀抱,使用Flex Builder 3配合AS3开发的确比Flex 1.x的时代先进了不少,更成熟却依然轻便的ActionScript 3/AVM 2环境让人用起来很舒服。一日我突然冒出一个问题:AS是否有反射机制?Google之,找到一个方法“flash.utils.describeType”,这个方法使用一个对象作为参数,以XML的形式返回此对象类型的信息。

有了这必要的反射机制,于是我想尝试实现一个AS环境下的单元测试框架,才几百行代码,一个具有基本功能的,类似xUnit的框架就写好了,下面我将介绍它几处关键的实现技巧,在文章最后有源代码下载。

1. 获取类型信息

首先先看看这个简单的TestFixture类:
package
{    
    import flash.errors.IOError;    
    import fxunit.
*;
    
    public class PersonTests
    {
        
var name:String = "Adrian";
        
var age:int = 20;
        
        private 
function createPerson():Person
        {
            
var person:Person = new Person(name, age);
            
return person;
        }
        
        [Test]
        public 
function TestPersonCtor():void
        {
            
var person:Person = createPerson();
            Assert.equal(person.name, name);
            Assert.equal(person.age, age);
        }
        
        [Test]
        public 
function TestPersonSayHello():void
        {
            
var person:Person = createPerson();
            Assert.equal(person.sayHello(), 
"Hello, I am Adrian, I am 20 years old.");
        }
        
        [Test]
        public 
function TestFailOnPurpose():void
        {
            Assert.equal(createPerson(), 
null);
        }
        
        [Test(expectedError
="flash.errors::IOError")]
        public 
function TestExpectedError():void
        {
            
throw new flash.errors.IOError();    
        }
    }
}

类中方法上面的[Test]看上去很像C#中的Attribute(属性),但它在AS当中叫Metadata(元数据),它们实现的实现机制和用途都有 些差别,但在这里它和C#的Attribute的用途一样,就是指明这个方法是一个Test,需要TestRunner来执行,并且指出了该Test 的期望异常。要注意的是,需要在Flex Compiler选项中加入“-keep-as3-metadata+=Test”才能使这个Metadata链接进binary中,才能在运行时获取。

对于以上的TestFixture,使用describeType方法将返回以下XML内容:
<type name="PersonTests" base="Object" isDynamic="false" isFinal="false" isStatic="false">
  
<extendsClass type="Object"/>
  
<method name="TestPersonSayHello" declaredBy="PersonTests" returnType="void">
    
<metadata name="Test"/>
  
</method>
  
<method name="TestFailOnPurpose" declaredBy="PersonTests" returnType="void">
    
<metadata name="Test"/>
  
</method>
  
<method name="TestPersonCtor" declaredBy="PersonTests" returnType="void">
    
<metadata name="Test"/>
  
</method>
  
<method name="TestExpectedError" declaredBy="PersonTests" returnType="void">
    
<metadata name="Test">
      
<arg key="expectedError" value="flash.errors::IOError"/>
    
</metadata>
  
</method>
</type>

可以看出这段XML完全可以为TestRunner提供足够的类型信息,包括Metadata。我使用以下方法解析这段XML,可以看到AS3操纵XML是很方便的。
protected static function fromTypeInfoXML(xml:XML):ClassInfo
{
    
var ci:ClassInfo = new ClassInfo();
    ci._name 
= xml.@name;
    
for each (var methodNode:XML in xml.method)
    {
        
var mi:MethodInfo = new MethodInfo(methodNode.@name);
        
for each (var meta:XML in methodNode.metadata)
        {
            
var mdi:MetadataInfo = new MetadataInfo(meta.@name);
            
for each (var arg:XML in meta.arg)
            {
                mdi.args.put(arg.@key.toString(), arg.@value.toString());
            }
            mi.metadata.push(mdi);
        }
        ci._methods.push(mi);
    }
    
return ci;
}
ClassInfo,MethodInfo,MetadataInfo是我自己写的类型,现在它们的功能很少,以后有时间我可能会写一个类似.NET System.Reflection,系统一点的反射类库。

2. 根据类型信息执行测试方法

以下是TestRunner类的全部代码:
package fxunit
{
    import flash.events.EventDispatcher;
    import flash.utils.getQualifiedClassName;
    
    import fxunit.reflect.ClassInfo;
    import fxunit.reflect.MetadataInfo;
    import fxunit.reflect.MethodInfo;
    import fxunit.reflect.typeinfo;
    
    
// Event元数据让你在键入addEventListener后会有相应的Event类型提示
    [Event(name="passed", type="fxunit.TestEvent")]
    [Event(name
="failed", type="fxunit.TestEvent")]
    public class TestRunner extends EventDispatcher
    {
        private 
var _testClass:Class;
        
        
// TestRunner只需要一个Class对象
        public function TestRunner(testClass:Class)
        {
            _testClass 
= testClass;
        }
        
        public 
function runTests():void
        {
            
var testObj:Object = new _testClass();
            
// 获取类型信息,这个方法在fxunit.reflect包中
            var ci:ClassInfo = typeinfo(_testClass);
            
            
for each(var m:Object in ci.methods)
            {
                
var mi:MethodInfo = MethodInfo(m);
                
// 检查其Metadata, 验证是否是一个Test方法
                if (isTestMethod(mi))
                {
                    
// 获取其Function对象
                    var method:Function = testObj[mi.name];
                    
// 检查其Metadata, 是否期望异常
                    var expectedError:String = getExpectedError(mi);
                    
try
                    {
                        
// 调用! 没有异常即通过测试
                        method.call(testObj);
                        dispatchTestEvent(TestEvent.PASSED, mi);
                    }
                    
catch (ae:AssertError)
                    {
                        
// 断言异常, 测试失败
                        dispatchTestEvent(TestEvent.FAILED, mi, ae);
                    }
                    
catch (e:Error)
                    {
                        
// 如果期望异常则检查是否类型相同
                        if (expectedError != null)
                        {
                            
var eName:String = getQualifiedClassName(e);
                            
if (eName == expectedError)
                            {
                                dispatchTestEvent(TestEvent.PASSED, mi);
                            }
                            
else
                            {
                                dispatchTestEvent(TestEvent.FAILED, mi, 
                                        
new AssertError("Expected: " + expectedError +
                                                        
", but was: " + eName));                    
                            }
                        }
                        
else // 否则测试失败
                        {
                            dispatchTestEvent(TestEvent.FAILED, mi, e);
                        }                        
                    }
                }
            }    
        }
        
        protected 
function dispatchTestEvent(type:String, mi:MethodInfo, e:Error = null)
        {
            dispatchEvent(
new TestEvent(type, mi.name, e ? e.message : null));
        }
        
        public static const TEST_METADATA 
= "Test";
        public static const EXPECTED_ERROR_ARG 
= "expectedError";
        
        
// 检查是否有名为Test的Metadata
        protected function isTestMethod(mi:MethodInfo):Boolean
        {
            
for each (var obj:Object in mi.metadata)
            {
                
var mdi:MetadataInfo = MetadataInfo(obj);
                
if (mdi.name == TEST_METADATA)
                {
                    
return true;
                }
            }            
            
return false;
        }
        
        protected 
function getExpectedError(mi:MethodInfo):String
        {            
            
for each (var obj:Object in mi.metadata)
            {
                
var mdi:MetadataInfo = MetadataInfo(obj);
                
if (mdi.name == TEST_METADATA && mdi.args.contains(EXPECTED_ERROR_ARG))
                {
                    
// 如果包含expectedError参数, 则返回参数值
                    return String(mdi.args.geti(EXPECTED_ERROR_ARG));
                }
            }        
            
return null;
        }
    }
}

TestRunner的逻辑很明了也很容易实现,这样我们就可以使用TestRunner来运行测试了:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute"
     applicationComplete
="onAppComplete()" width="842" height="263">
    
<mx:Script>
        
<![CDATA[
            import fxunit.reflect.*;
            import fxunit.
*;
        
            
function onAppComplete():void
            {
                
var tr:TestRunner = new TestRunner(PersonTests);
                tr.addEventListener(TestEvent.PASSED, onTestPassed);
                tr.addEventListener(TestEvent.FAILED, onTestFailed);
                tr.runTests();
            }
            
            
function onTestPassed(te:TestEvent):void
            {
                write(
"[PASSED]: " + te.testName + (te.testMessage ? ""n"t" + te.testMessage : ""));
            }
            
            
function onTestFailed(te:TestEvent):void
            {
                write(
"[FAILED]: " + te.testName + (te.testMessage ? ""n"t" + te.testMessage : ""));
            }
            
            
function write(msg:String):void
            {                
                out.text 
+= msg + ""n";
            }   
             
        
]]>
    
</mx:Script>
    
<mx:TextArea x="10" y="10" width="818" height="240" id="out"/>
</mx:Application>

对于PersonTests,程序输出如下:
[FAILED]: TestPersonSayHello
    Expected: Hello, I am Adrian, I am 20 years old., but was: Hello, my name is Adrian, I am 20 years old.
[FAILED]: TestFailOnPurpose
    Expected: null, but was: [object Person]
[PASSED] : TestPersonCtor
[PASSED] : TestExpectedError

用AS实现简单的TestRunner就是这样,明了也很简单。但一个具有完整功能的单元测试框架绝不仅仅是这点功能,事实上已经有面向AS测试的开源框架了,例如FlexUnit和ASUnit,但我还都没看过它们的代码,它们能和NUnit、JUnit媲美么?

源代码下载:
http://files.cnblogs.com/Dah/fxunit_src.zip
Posted on 2007-10-21 19:37 Adrian H. 阅读(...) 评论(...) 编辑 收藏