如果你最近或者以前做过单元测试,那么你肯定觉得这玩意有时候还真是挺困难的。可能你会遇到下面的重重挑战:
- 你需要在不深入细节的情况下,确保某些事件的发生。
- 写一个单项测试太费劲了。
- 有些测试根本不在你的掌控之内。
所以我想到了使用mock 对象 来绕过这些单元测试的绊脚石。说白了,mock对象就是可以模拟其他对象行为的一种对象,它可以使单元测试易于编写。
想象一下自己正开始投身于一项新技术...
你们公司正打算在明天发布一个在线汽车交易网站。早上十点,你的老板满脸忧愁地站在你的办公桌旁,原因是他居然忘了加入允许用户在网站发布汽车信息的重要功能。晕!所以你的任务就是在今天结束前把这件事搞定,否则你就得拍屁股走人。
于是,你看了看代码,发现服务的方法已经存在,并且可以使用:
package com.compact.mocking
{
public
class VehicleService
{
//发布方法
public
function
publish
( item
: Vehicle
)
:
void
{
...
}
}
}
太棒了,现在要做的事就是确保从GUI上能够调用这个方法。
package com.compact.mocking
{
public
class VehicleModel
{
public
var service
: VehicleService;
//发布方法
public
function
publish
( item
: Vehicle
)
:
void
{
service.
publish
( item
) ;
}
}
}
很简单,下一步就要开始单元测试,确保我们调用了正确的方法。这听上去可能有些傻,因为我们已经知道VehicleService能用,所以我们根本没必要测试。我们真正要做的是确保调用了这个方法,其他的我们并不关心。
你不必深入就可以需要确保某些事件的发生。
我们使用mockito-flex 来创建我们服务的mock版本。我们要做的是用mock版本代替真实版本。多亏了mockito-flex ,我们的mock可以自动记录所有接受到的调用。当publish方法被调用时,我们可以检测到这些记录以便校验。
package com.compact.mocking
{
import org.mockito.MockitoTestCase;
// MockitoTestCase继承标准的FlexUnit TestCase.
public
class CustomerModelTest extends MockitoTestCase
{
private
var _model
: VehicleModel;
public
function CustomerModelTest
(
)
{
//告诉mockito哪个类需要被mock.
super
(
[ VehicleService
]
) ;
}
override
public
function setUp
(
)
:
void
{
_model =
new VehicleModel
(
) ;
// 用mock版本替换真实版本.
_model.service = mock
( VehicleService
) ;
}
public
function testPublishDelegatesToService
(
)
:
void
{
var item
: Vehicle =
new Vehicle
(
) ;
// 调用我们mock服务的publish方法.
_model.
publish
( item
) ;
// 校验mock服务接受到正确的调用.
verify
(
) .that
( _model.service.
publish
( item
)
) ;
}
}
}
你在5分钟内搞定了一切,令老板对你刮目相看!不过很不幸,胜利十分短暂,因为他又想起了什么...没错,在发布前你必须确保将要发布的汽车信息是有效的。
于是,你又看了看代码,发现确认有效的方法已经存在:
package com.compact.mocking
{
[ Bindable
]
public
class Vehicle
{
public
var make
:
String ;
public
var model
:
String ;
public
var price
:
Number ;
public
var ownerName
:
String ;
public
var locationName
:
String ;
public
function isValid
(
)
:
Boolean
{
var valid
:
Boolean =
true ;
valid = valid
&& notEmpty
( make
) ;
valid = valid
&& notEmpty
( model
) ;
valid = valid
&& notEmpty
( ownerName
) ;
valid = valid
&& notEmpty
( locationName
) ;
valid = valid
&& price
>
0 ;
return valid;
}
private
function notEmpty
(
value
:
String
)
:
Boolean
{
return
value
&&
value .
length
>
0
}
}
}
所以你修改了模型以便好好利用这个方法。
package com.compact.mocking
{
public
class VehicleModel
{
public
var service
: VehicleService;
public
function
publish
( item
: Vehicle
)
:
void
{
if
( item.isValid
(
)
)
{
service.
publish
( item
) ;
}
}
}
}
然后很专业的修改了测试。
package com.compact.mocking
{
import org.mockito.MockitoTestCase;
public
class CustomerModelTest extends MockitoTestCase
{
private
var _model
: VehicleModel;
public
function CustomerModelTest
(
)
{
super
(
[ VehicleService
]
) ;
}
override
public
function setUp
(
)
:
void
{
_model =
new VehicleModel
(
) ;
_model.service = mock
( VehicleService
) ;
}
public
function testPublishDelegatesToService
(
)
:
void
{
var item
: Vehicle =
new Vehicle
(
) ;
item.make =
"a" ;
item.model =
"b"
item.price =
5 ;
item.ownerName =
"fred" ;
item.locationName =
"australia"
_model.
publish
( item
) ;
verify
(
) .that
( _model.service.
publish
( item
)
) ;
}
}
}
工作终于完了,但是你感觉做的工作太多了。你突然记起你的同事Fred正在为确认有效方法加了一些条件,这意味着你还得继续补全这部分测试。
你得费很大劲才能完成一个单项测试。
你希望校验汽车信息不必如此的麻烦。如果用mock创建一个一直都有效的汽车信息岂不很好?没问题,当然可以
package com.compact.mocking
{
import org.mockito.MockitoTestCase;
public
class CustomerModelTest extends MockitoTestCase
{
private
var _model
: VehicleModel;
private
var _item
: Vehicle;
public
function CustomerModelTest
(
)
{
// 同样mock汽车.
super
(
[ VehicleService, Vehicle
]
) ;
}
override
public
function setUp
(
)
:
void
{
_model =
new VehicleModel
(
) ;
_model.service = mock
( VehicleService
) ;
// 创建mock汽车.
_item = mock
( Vehicle
) ;
}
public
function testPublishDelegatesToService
(
)
:
void
{
// 更新mock让isValid()isValid()方法永远返回true
given
( _item.isValid
(
)
) .willReturn
(
true
) ;
_model.
publish
( _item
) ;
verify
(
) .that
( _model.service.
publish
( _item
)
) ;
}
}
}
现在老板彻底被你折服了... 你只不过修改了5行测试代码,却没让Fred的更新对你产生任何影响。
哈哈,一切顺利,你正筹划着一次大的提交。现在是下午3点,你正准备喝一杯庆祝一下,然而你的老板再次想起了一些事... 你还得记录每次汽车信息发布的日期,因为用户每天在网站发布一次信息就收费5毛钱。
神奇,服务中已经存在此方法了!
package com.compact.mocking
{
public
class VehicleService
{
public
function
publish
( item
: Vehicle
)
:
void
{
...
}
public
function recordPublishTime
( item
: Vehicle,
time
:
Date
)
:
void
{
...
}
}
}
没问题,我们只需要在模型中加入对此方法的调用。
package com.compact.mocking
{
public
class VehicleModel
{
public
var service
: VehicleService;
public
function
publish
( item
: Vehicle
)
:
void
{
if
( item.isValid
(
)
)
{
service.
publish
( item
) ;
service.recordPublishTime
( item,
new
Date
(
)
) ;
}
}
}
}
好了,看起来不错,唯一的问题是我们如何使用mockito来验证此次调用呢?
暂且先试试吧:
public
function testPublishRecordsPublishTimeUsingService
(
)
:
void
{
given
( _item.isValid
(
)
) .willReturn
(
true
) ;
_model.
publish
( _item
) ;
verify
(
) .that
( _model.service.recordPublishTime
( _item,
new
Date
(
)
)
) ;
}
不幸的是,这根本运行不了,因为校验里用到的数据和模型中创建的数据根本就是不同的日期值,那么你该如何控制日期创建流程以便完成测试呢?
有时候你需要测试一些你无法控制的东西。
幸运的是,这并不难,我们需要一个数据工厂。一旦有了这个工厂,我们就可以使用mock来控制日期创建流程了,就是下面的工厂:
package com.compact.mocking
{
public
class TimeFactory
{
public
function currentTime
(
)
:
Date
{
return
new
Date
(
) ;
}
}
}
然后,我们修改模型,在模型使用此工厂,而不再使用直接日期创建。
package com.compact.mocking
{
public
class VehicleModel
{
public
var service
: VehicleService;
public
var timeFactory
: TimeFactory =
new TimeFactory
(
) ;
public
function
publish
( item
: Vehicle
)
:
void
{
if
( item.isValid
(
)
)
{
service.
publish
( item
) ;
service.recordPublishTime
( item, timeFactory.currentTime
(
)
) ;
}
}
}
}
现在我们可以轻松的使用mock工厂来完成测试了。
package com.compact.mocking
{
import org.mockito.MockitoTestCase;
public
class CustomerModelTest extends MockitoTestCase
{
private
var _model
: VehicleModel;
private
var _item
: Vehicle;
public
function CustomerModelTest
(
)
{
super
(
[ VehicleService, Vehicle, TimeFactory
]
) ;
}
override
public
function setUp
(
)
:
void
{
_model =
new VehicleModel
(
) ;
_model.service = mock
( VehicleService
) ;
_model.timeFactory = mock
( TimeFactory
) ;
_item = mock
( Vehicle
) ;
}
public
function testPublishRecordsPublishTimeUsingService
(
)
:
void
{
var publishDate
:
Date =
new
Date
(
) ;
given
( _item.isValid
(
)
) .willReturn
(
true
) ;
given
( _model.timeFactory.currentTime
(
)
) .willReturn
( publishDate
) ;
_model.
publish
( _item
) ;
verify
(
) .that
( _model.service.recordPublishTime
( _item, publishDate
)
) ;
}
}
}
漂亮! 你搞定了。
你刚刚完成了拯救公司的壮举!你成功地在网站中添加了允许用户发布汽车信息的功能,并且使用了mock对象和mockito-flex 框架完成了单元测试。虽然当中遇到了些困难,但问题全都都迎刃而解。现在你得再接再厉,继续完成更多精彩的测试。
www.taskcity.com
taskcity.com将您的技术转化为美金。