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