使用ActionScript 3中的反射,实现单元测试TestRunner

本文介绍如何利用ActionScript 3中的反射机制实现一个简单的单元测试框架,包括获取类型信息和执行测试方法的关键技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近要用到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://www.cnblogs.com/Files/Dah/fxunit_src.zip  
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值