Stubs vs. Mocks
让我们通过老虎机这一例子,看一看stubs和mock对象。由于这个单元测试关注单个对象,我们创建一个老虎机,并把它当作被测系统。现在让我们写一个简单的测试,生成老虎机。
function testRender() {
var buttonStub = {};
var balanceStub = {};
var reelsStub = [{},{},{}];
var randomNumbers = [2, 1, 3];
var randomStub = function(){return randomNumbers.shift();};
var slotMachine = new drw.SlotMachine(buttonStub, balanceStub, reelsStub,
randomStub);
slotMachine.render();
assertEquals('Pay to play', buttonStub.value);
assertTrue(buttonStub.disabled);
assertEquals(0, balanceStub.innerHTML);
assertEquals('images/2.jpg', reelsStub[0].src);
assertEquals('images/1.jpg', reelsStub[1].src);
assertEquals('images/3.jpg', reelsStub[2].src);
}
testRender函数使用了两个DOM元素的stub,把它们都注入到被测系统的构造函数中,并调用render方法。测试的最后对 render方法的期望结果进行断言。请注意通过使用DOM元素的stub,我们可以测试render方法的结果,而不必实际去做任何事情,这些事情可能导致测试页面的其他测试失效。这种方法与使用真实DOM元素各有利弊。使用真实的DOM元素更容易发现跨浏览器不兼容的bug,但是如果每个测试最后或者 tearDown时没有进行重置,你的测试本身也更容易带来bug。
被测系统并未直接调用全局函数Math.random,来决定每个轴初始的图片状态。相反,老虎机是依赖创建时提供给它的参数,来得到这些数字。这让我们可以测试一段不确定的代码,好像完全可以预测一样。请注意测试没有覆盖浏览器原生的Math.random实现,从而避免了状态变化的风险和副作用。
等等,等一会儿... 测试函数有不止一个断言,这样行吗?敏捷社区中部分人认为每个测试中有多于一个断言是邪恶的。然而,给用来赚钱的实际的应用程序很少会这么写测试套件。当亲眼看到JUnit框架本身实物测试套件中每个测试有多少断言时,相信会有很多人非常惊讶。
对象的构造函数和render方法看上去是这样的:
/**
* Constructor for the slot machine.
*/
drw.SlotMachine = function(buttonElement, balanceElement, reels, random, networkClient)
{
this.buttonElement = buttonElement;
this.balanceElement = balanceElement;
this.reels = reels;
this.random = random;
this.networkClient = networkClient;
this.balance = 0;
};
drw.SlotMachine.prototype.render = function() {
this.buttonElement.disabled = true;
this.buttonElement.value = 'Pay to play';
this.balanceElement.innerHTML = 0;
for(var i = 0; i < this.reels.length;){
this.reels[i++].src = 'images/' + this.random() + '.jpg';
}
};
让我们往老虎机里放一些钱。在这个场景下,老虎机异步调用服务器端以返回用户余额。这很有挑战性,因为单元测试中没有网络,AJAX调用会失败。当我们编写单元测试时,我们应该尽量编写没有副作用的代码,IO当然也属于这一类。
function testGetBalanceGoesToNetwork(){
var url, callback;
var networkStub = {
send : function() {
url = arguments[0];
callback = arguments[1];
}
};
var slotMachine = new drw.SlotMachine(null, null, null, null, networkStub);
slotMachine.getBalance();
assertEquals('/getBalance.jsp', url);
assertEquals('function', typeof callback);
}
这个测试使用了网络stub。什么是stub呢?stub与mock有什么区别呢?许多开发者经常混淆这个两个词,认为它们是同义词。测试社区中,stub这个词是保留给基于状态测试的。JavaScript中它通常是指一个简单的object literal,能够返回预先硬编码的数值。而mock这个词则是保留给交互测试的。Mock可以针对行为训练。这些行为与被测对象进行交互,并且可以验证这些交互。
通过网络客户端stub,我们现在能够测试getBalance方法。通过本地变量url和callback,应用于构造函数的object literal stub能够记录它与被测系统的交互行为。这些本地变量使我们能够在测试的最后执行断言。不幸的是,我们用错工具了。这是一个经典例子,说明了stub的局限性,以及为什么使用mock对象。这个测试的目的不是在给了一定状态之后,验证被测系统的行为。测试关注的是drw.SlotMachine实例与它的一个协作者——网络客户端之间的交互。