惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Simon Willison's Weblog
Simon Willison's Weblog
P
Privacy International News Feed
www.infosecurity-magazine.com
www.infosecurity-magazine.com
T
Troy Hunt's Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
Attack and Defense Labs
Attack and Defense Labs
S
Secure Thoughts
V2EX - 技术
V2EX - 技术
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
O
OpenAI News
Cloudbric
Cloudbric
Google Online Security Blog
Google Online Security Blog
Schneier on Security
Schneier on Security
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Help Net Security
Help Net Security
Cyberwarzone
Cyberwarzone
G
GRAHAM CLULEY
L
Lohrmann on Cybersecurity
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Spread Privacy
Spread Privacy
NISL@THU
NISL@THU
N
News and Events Feed by Topic
T
Tenable Blog
S
Security @ Cisco Blogs
N
News and Events Feed by Topic
The Hacker News
The Hacker News
C
CXSECURITY Database RSS Feed - CXSecurity.com
宝玉的分享
宝玉的分享
月光博客
月光博客
酷 壳 – CoolShell
酷 壳 – CoolShell
美团技术团队
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Google DeepMind News
Google DeepMind News
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Tailwind CSS Blog
V
Visual Studio Blog
P
Proofpoint News Feed
Webroot Blog
Webroot Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
博客园 - 三生石上(FineUI控件)
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Jina AI
Jina AI
雷峰网
雷峰网
T
The Blog of Author Tim Ferriss
Hugging Face - Blog
Hugging Face - Blog
腾讯CDC
L
LangChain Blog
The Register - Security
The Register - Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 聂微东

博客园 - Adrian H.

Hello in 2025 Hello World 为Silverlight控件添加鼠标滚轮支持 - Adrian H. - 博客园 LINQ表达式中关于显式范围变量的Bug jQuery太好使了 一次性下载.NET源码和pdb,并在VS2005中进行调试 Windows Server 2008: Server Core初体验 - Adrian H. C# Future Focus: 动态查找(Dynamic Lookup) Volta : Democratizing the Cloud, 1st CTP Released! Parallel Extensions for .NET 3.5 CTP! Silverlight 2.0, .NET类库源码, ASP.NET MVC... VS 2008与老版本插件的兼容性问题导致Crash Silverlight Tools for VS 2008 RTM [C# 3.0] 传递匿名类型对象的问题 Visual Studio 2008 RTM! 在 .NET Framework x (x < 3.5) 环境下运行带有扩展方法(Extension Method)的程序 ASP.NET MVC Framework Announced .NET Framework 源代码将于今年发布 N-Tiers?
使用ActionScript 3中的反射,实现单元测试TestRunner
Adrian H. · 2007-10-21 · via 博客园 - Adrian H.

最近要用到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媲美么?

源代码下载:
https://files.cnblogs.com/Dah/fxunit_src.zip