最近要用到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类:
类中方法上面的[Test]看上去很像C#中的Attribute(属性),但它在AS当中叫Metadata(元数据),它们实现的实现机制和用途都有 些差别,但在这里它和C#的Attribute的用途一样,就是指明这个方法是一个Test,需要TestRunner来执行,并且指出了该Test 的期望异常。要注意的是,需要在Flex Compiler选项中加入“-keep-as3-metadata+=Test”才能使这个Metadata链接进binary中,才能在运行时获取。
对于以上的TestFixture,使用describeType方法将返回以下XML内容:
可以看出这段XML完全可以为TestRunner提供足够的类型信息,包括Metadata。我使用以下方法解析这段XML,可以看到AS3操纵XML是很方便的。
2. 根据类型信息执行测试方法
以下是TestRunner类的全部代码:
TestRunner的逻辑很明了也很容易实现,这样我们就可以使用TestRunner来运行测试了:
对于PersonTests,程序输出如下:
用AS实现简单的TestRunner就是这样,明了也很简单。但一个具有完整功能的单元测试框架绝不仅仅是这点功能,事实上已经有面向AS测试的开源框架了,例如FlexUnit和ASUnit,但我还都没看过它们的代码,它们能和NUnit、JUnit媲美么?
源代码下载:
https://files.cnblogs.com/Dah/fxunit_src.zip
有了这必要的反射机制,于是我想尝试实现一个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();
}
}
}
{
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>
<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,系统一点的反射类库。{
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;
}
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;
}
}
}
{
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>
<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
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