本文关注于宏观上的CI和单元测试技术,某些技术上的具体细节会略过,更多细节请参考文中的链接。
本文包括:持续集成、单元测试、Mock技术、Case选取策略和示例等五部分
持续集成(CI)
CI是一种实践,旨在缓和和稳固软件的构建过程,能够应对如下挑战:
- 软件构建自动化
- 持续自动的构建检查
- 持续自动的构建测试(本篇文章的重点所在)
- 构建生成后续过程的自动化
hudson及搭建
一个非常流行的CI工具,易于安装及配置,学习成本低,本篇中hudson安装及配置在windows平台进行。
安装JDK
从www.java.com下载直接安装,然后设置java相关的环境变量:
- JAVA_HOME:jdk安装目录,例如:c:\java\jdk1.7.0
- PATH:使得系统在任何路径下可以识别java命令,设为:%JAVA_HOME%/bin;%JAVA_HOME%/jre/bin
- CLASSPATH:为java加载类(class or lib)路径,只有类在classpath中,java命令才能识别,设为:.;%JAVA_HOME%/lib/dt.jar;%JAVA_HOME%/lib/tools.jar
(要加.表示当前路径)
安装ANT
1.从http://ant.apache.org/下载ant1.8.2
2.解压到任意目录,设置环境变量ANT_HOME指向ant主目录
3.设置环境变量ANT_OPTS,值为-XX:MaxPermSize=128M
-Xms128M -Xmx512M分配更大的内存空间
安装Hudson
1.首先下载hudson.war http://http://www.hudson-ci.org/
2.其次通过 java -jar hudson.war命令运行
3.由于本身内置http服务,在浏览器中打开http://localhost:8080查看hudson是否运行
4.持续运行方法
简明使用方法
添加节点并进行设置
可以理解为一个项目,以下为admaker为例。 需要更多节点,请点击”New
Node
节点设置,主要设置”Remote FS root”和“Launch Method”即可。
添加job并进行设置
可以理解为创建项目的一个任务,例子为auto,使用下图选项即可:
集成flexunit/pmd/cpd
hudnson通过ant进行自动化编译,把编译的结果设置为hudson的读取源,运行原理如下:
自动化编译设置
拷贝sdk\x.x.x\ant\lib下的flexTasks.jar至ant\lib目录下,并在build.xml中通过以下指令指定:
<taskdef resource="flexTasks.tasks"
classpath="${FLEX_HOME}/ant/lib/flexTasks.jar"
/>
单元测试所需文件设置
从https://github.com/flexunit/flexunit下载源代码,通过ant进行自动化编译,生成类似flexUnitTasks-4.1.0-x.jar的文件,拷贝到
项目所在的libs文件夹,并在build.xml中通过以下指令指定:
<taskdef resource="flexUnitTasks.tasks">
<classpath>
<fileset dir="${lib.loc}">
<include name="flexUnitTasks*.jar"
/>
</fileset>
</classpath>
</taskdef>"
PMD/CPD
flexpmd,主要用来提升Flex/AS3源文件中的代码质量并且检测常见的不好的代码实践,比如无用的代码,效率低的代码片段,过于复杂的代码等等这些都是FlexPMD检测并报告的对象。
flexcpd,Flex/as3源文件中的代码重复率检查工具。 下载利用ant编译所需要的文件,最新版为1.2:http://opensource.adobe.com/wiki/display/flexpmd/Downloads
配置(flexpmd):http://opensource.adobe.com/wiki/display/flexpmd/How+to+invoke+FlexPMD
配置(flexcpd):http://opensource.adobe.com/wiki/display/flexpmd/FlexCPD
条件编译参数的方法
在flex-config.xml文件中的<compile></compile>中添加以下内容:
<define>
<name>CONFIG::debug</name>
<value>false</value>
</define>
<define>
<name>CONFIG::embed</name>
<value>false</value>
</define>
<define>
<name>CONFIG::edit</name>
<value>true</value>
</define>
构建
点击“Build Now”即可看到相应结果
windows下默认端口1024被占用问题
中的属性port是指定socket server的端口号,而CIListener中的默认端口号是1024,此时需要做的是:
修改CIListener.as和VisualDebuggerListener.mxml中的默认端口号为9900,在flexunit项目目录中执行ant编译,在FlexUnit4Test\libs中找到flexunit-cilistener-41.0-1-4.5.0.0.swc文件,放到你的项目相应的地方即可,此时要注意显示指定port=”9900″。
为了不显示指定端口号9900,需在TestRunConfiguration.java中修改默认端口号为9900,在flex项目目录中执行ant编译,在FlexUnit4Test\libs\build中找到flexUnitTasks-4.1.0-1.jar文件,放到你的项目的相应的位置即可。
建议把完成以上两个步骤的操作
单元测试
flexunit4
进化史
as2unit 2003
flex1.0 flexunit
flex2.0 flexunit.9 as3—> as3flexunit(google
code)—–>back to adobe
dpUInt—〉Fluint
flexunit4 2009
框架原理图
使用方法
1.在项目上点击右键,添加test case 或test suite(为case套装,包含n个case),添加相应的文件,flash
builder会为你自动创建测试用主类FlexUnitApplication.as
2.在项目上点击右键“执行单元测试”或通过单元测试面板执行,选择相应的test
case 或者test suite运行即可。
3.一般的结构如下,把AM2Test作为唯一的入口即可:
基础知识
testMethod,testCase,testSuite
结构图如下:
testMethod
最小的测试单元,顾名思义,针对类的方法的测试,使用[Test]元标签,示例如下:
[Test]
public function testUpload(){};
策略:
1.一般为要至少为类中的每个方法写一个testMethod
2.对于一个方法,由于不同的条件逻辑可能会产生不同的结果,针对每一个结果写一个testMethod
3.针对没一个方法,写两个testMethod,针对有效和无效的输入
TestCase
testMethod的集合,测试多个相关的功能点,一般针对某一个类,其中包含所有的需要测试的testMethod,包含如下特有的元数据标签,可以重复书写:
[Before]
[Before Class]
[After]
[After Class]
[Test]
其中每个[Test]是可以有顺序的,通过order属性指定,形如:
[Test(order=1)]
public function testOrder1(){};
[Test(order=2)]
public function testOrder2(){};
TestSuite
testCase的集合,使用如下元数据标签标记,示例如下:
[Suite]
[RunWith("org.flexunit.runners.Suite")]
public class MultiFileUploaderSuite{
public var _testUploadList:TestUploadList;
}
断言
断言用于比较测试目标的结果和预期值,一般格式如下:
assert( "错误提示", 预期值, 实际值 );
示例:
result = 1 + 2;
assertEqual( "结果不为3", 3, result
);
有两种使用格式:
hamcrest
开源的匹配器库,格式如下:
assertThat( value, matcher );
其中,matcher是一个类,有is(),between(),closeTo()等
用法举例:
assertThat( num1, is( between( num2,
num3 ) ) );
自定义matcher:
import org.hamcrest.TypeSafeMatcher;
import org.hamcrest.Description;
public class myMatcher exends TypeSafeMatcher{
public function myMatcher(){};
override public function matchesSafely(item:Object):Boolean
{};
override public function describeTo(description:Description):void
{};
}
异步测试
flexunit4的核心功能,想想flash中的各种异步事件吧!包含两种格式,事件处理和事件序列(sequence)。
事件处理示例代码:
[Test(async, timeout=5000)]
public function testCancel():void{
var mHandler:Function = Async.asyncHandler(
this, cancelHandler,5000, {num:4,type:"m"},
timeoutHandler );
_uploader.addEventListener( UploaderEvent.EVENT_FILE_ALL_MEMEORY,mHandler
);
_uploader.loadtoMemory();
}
如示例所示:
- async:必须存在,说明是异步测试
- timeout=5000, 在5000毫秒内测试未完成,默认超时处理
mHandle是异步处理对象,包含5个参数,意义如下:
- this:针对哪个TestCase,这里即为this
- cancelHandler,接收到UploaderEvent.EVENT_FILE_ALL_MEMORY事件后触发的事件对象
- 5000,超时处理触发的最大时间
- {num:4,type:”m”},传递给cancelHandler对象的参数
- timeoutHandler,超时处理对象,5000ms后未触发cancelHandler对象触发
事件序列,是一个很实用的使用格式,例如测试一个文件上传过程中取消的功能,会触发的事件有上传,取消成功,取消失败等,这个时候假如用事件处理的格式书写的话,会涉及到n个处理函数,很复杂,而用sequence呢,显然一目了然。
事件序列示例代码:
[Test( async )]
public function shouldCompleteTimerSequence():void {
var timer:Timer = new Timer( TIMEOUT );
var sequence:SequenceRunner = new SequenceRunner( this );
sequence.addStep( new SequenceCaller(timer, timer.start ) );
sequence.addStep( new SequenceWaiter(timer, TimerEvent.TIMER, TIMEOUT2 ) );
sequence.addStep( new SequenceWaiter(timer, TimerEvent.TIMER, TIMEOUT2 ) );
sequence.addStep( new SequenceWaiter(timer, TimerEvent.TIMER, TIMEOUT2 ) );
sequence.addStep( new SequenceCaller( timer, timer.stop );
sequence.addAssertHandler( handleSequenceComplete, null );
sequence.run();
}
如示例所示,执行步骤如下:
开始一个定时器
循环执行三次
停止定时器
执行断言
Rules
类似于[Before]和[After]的元数据标签,提供更多高级功能,定义每个test运行前后的规则,在每个test之前和之后运行,结构如下:
-BeforeClasses
-Rules
-Befores
-Test
-Afters
-Rules (the same ones as above)
-AfterClasses
UIImpersonator
借鉴Fluint框架的测试内容的显示对象,通过它你可以把可视化的内容暂时在舞台上,其实个人认为实用性不大,和自动化的思想有违背。在纯as3项目中暂时无法显示,不过可以利用这个技巧实现:https://gist.github.com/1094408
Runner和自定义Runner
规定了Test Method、Test Case和Test Suite运行时的行为,兼容性好,能很好的运行flexunit1和fluint框架的测试内容。有不同责任的Runner,最常用的就是Suite。当测试单元运行的时候,ClassRequest对象会根据每个Test
Case和Test Suite的内容进行包装成为IRequest对象,并为此对象构建相应的Runner,FlexUnitCore.run(
ClassRequest)会执行所有的IRequest对象,触发IRequest的run()方法,执行相应的Runner.run()。
系统自定义的Runner有:
- IgnoredClassRunner
- Suite
- TheoryBlockRunner
- SuiteMethod
- FlexUnit1ClassRunner
- Fluint1ClassRunner
- BlockFlexUnit4ClassRunner
- ParentRunner
要自定义Runner,可以实现以下方法:
import org.flexunit.runner.IDescription;
import org.flexunit.runner.IRunner;
import org.flexunit.runner.notification.IRunNotifier;
import org.flexunit.token.IAsyncTestToken;
public class CustomRunner implements IRunner {
public function CustomRunner() {}
public function run(notifier:IRunNotifier,
previousToken:IAsyncTestToken):void
{}
public function get description():IDescription {}
public function pleaseStop():void {}
}
UIListener
Runner的监听对象,对监听内容进行图形化表示,过程如下:
import sampleSuite.SampleSuite;
import org.flexunit.listeners.UIListener;
import org.flexunit.runner.FlexUnitCore;
private var core:FlexUnitCore;
public function runMe():void {
core = new FlexUnitCore();
core.addListener( new UIListener()
);
core.run( sampleSuite.SampleSuite
);
}
CIListener
Runner的监听对象,不需要进行图形化的表示,说要做的就是把监听到的内容通过Socket发送给服务端,把内容写到磁盘,供Hudson读取处理,过程如下:
core.addListener(new CIListener());
core.run( sampleSuite.SampleSuite )}
Mockolate
原理
对象的一个代理对象,包含原对象的所有公共方法和属性,mock对象的父类为原对象类,这样就可以在使用原对象的地方使用mock对象。
由于mock技术是一门独立的技术,在此不作详述,请参考:http://www.mockolate.org/
使用
自定义Runner,自定义Rule,以及Mock标签:
[RunWith("mockolate.runner.MockolateRunner")]
public class TestUploader{
[Rule]
public var mocks:MockolateRule = new MockolateRule();
[Mock(type="strict")]
public var fileReference:FileReference;
[Mock(type="strict")]
public var fileReferenceList:FileReferenceList;
}
如上所示,在运行这个TestCase的时候使用MockolateRunner,自定义MockoateRule,通过Mock标签指定要mock的类,并指定为strict模式。
接下来做的是在TestMethod中使用它们:
var frl:FileReferenceList = strict( FileReferenceList
);
mock( frl ).getter( "fileList").returns(
[] );
mock( frl ).method("toString").returns( "FileReferenceList"
);
mock( frl ).method( "browse" ).args( Array
).dispatches( new Event( Event.SELECT, false, false
) );
_uploader.fileReferenceList = frl;
如上所示,
getter方法fileList的返回为一个[],
toString()方法返回“FileFeferenceList”,
调用browse()方法时,指定类型为Array的参数,并派发SELECT事件。
最后,通过_uploader.fileReferenceList = fr1,把Mock对象传递给_uploader对象。
Case选择策略
- 针对接口,但不针对getter/setter方法
- 核心功能点,比如上传模块中的上传、中断等功能
- 逻辑复杂的功能点,if-else、switch-case等
- 针对功能点,可能组合各函数,在时间有限的情况下,非核心功能点可以省去
- 根据bug书写case,复现步骤,并验证之
- 不针对样式和展现等相关的功能点,例如,“当文件名为20个中文的时候,上传列表显示错乱”这样的问题,应完全由QA保证,属于非单测范围。
- 在单测开始之前,开发人员应和QA沟通确认哪些case是由QA保证,哪些case是由开发人员单测保证
示例展示
以多文件上传核心类Uploader为例,要想测试此类必须要使用mock技术,因为FileReference的data为只读属性
根据选取策略,最终锁定在来Uploader类的核心方法,并建立如下的case:
testUploadedToMemory()
testUploadedALLComplete()
testUploadedALLError()
testCancel()
testResume()
testDelete()
每个case都会对FileReference、FileReferenceList和用于和AMF后端通讯的核心类AMFPHP进行mock,
testCancel()的示例代码如下:
/**
* 取消上传,上传完成2个后,cancel,验证以上传的ID列表的值是否为2
*
*/
[Test(async, order=4)]
public function testCancel():void{
_count = 0;
mockFileReference();
mockAMFPHP( true );
var mHandler:Function = Async.asyncHandler(
this, cancelHandler, 5000, {num:4,type:"m"},
timeoutHandler );
_uploader.addEventListener( UploaderEvent.EVENT_FILE_ALL_MEMEORY,
mHandler );
_uploader.addFileType( "test"
);
_uploader.browse();
}
FileReferenceHeFileReferenceList的mock代码:
/**
* mock reference相关的类
*
*/
private function mockFileReference():void{
var frl:FileReferenceList = strict(
FileReferenceList );
var num:int = 4;
var arr:Array = [];
for( var i:int; i < num; i++
){
var fr:FileReference = strict( FileReference );
mock( fr ).getter( "data"
).returns( _ba );
mock( fr ).getter( "type"
).returns( ".jpg" );
mock( fr ).getter( "size"
).returns( 10000 );
mock( fr ).getter( "name"
).returns( "fr" );
mock( fr ).method( "toString"
).returns( "FileReference" );
mock( fr ).method("load")
.dispatches( new Event( Event.COMPLETE,
false,false ) )
.dispatches( new IOErrorEvent( IOErrorEvent.IO_ERROR,
false, false ) );
arr.push( fr );
}
mock( frl ).getter( "fileList").returns(
arr );
mock( frl ).method("toString").returns(
"FileReferenceList" );
mock( frl ).method( "browse"
).args( Array )
.dispatches( new Event( Event.SELECT,
false, false ) );
_uploader.fileReferenceList = frl;
}
事件处理cancelHandler方法如下:
/**
* 上传过程中中断处理
*
* @param e
* @param pd
*
*/
private function cancelHandler(e:UploaderEvent, pd:Object):void{
var sHandler:Function;
switch( pd.type ){
case "m":
sHandler = Async.asyncHandler( this,
cancelHandler, 5000, {num:4, type:"a"}, timeoutHandler
);
_uploader.addEventListener( UploaderEvent.EVENT_FILE_COMPLETE,
sHandler );
_uploader.upload();
break;
case "a":
if( ++_count == 2 ){
_uploader.cancel();
Assert.assertEquals( "cancel失效:",
2, _uploader.uploadedIDAll.length );
}
}
}
如上所示:
调用_uploader.browse(),
派发Event.SELECT事件
Uploader内部把文件内容加载到内存,派发UploaderEvent.EVENT_FILE_ALL_MEMEORY事件
在CancelHandler中处理UploaderEvent.EVENT_FILE_ALL_MEMEORY事件
假如pd的属性type为m,进行上传
通过_count变量判断上传了两次,调用_uploader.cancle()方法
进行Assert,判断上传成功的数组列表的长度是否为2
失败的话,输出”cancel失效: ”
|