技术开发 频道

使用JsUnit和JSMock的JavaScript测试驱动开发

  JsMock,JavaScript的Mock对象库

  仔细查看你会发现testGetBalanceGoesToNetwork创建了自己的微型mocking框架。现在让我们重构测试,使用一种通用的mocking框架。我们需要在测试页面添加一个独立的脚本标签,并像这样重写测试:

  function testGetBalanceWithMocks(){

  var mockControl = new MockControl();

  var networkMock = mockControl.createMock({

  send : function() {}

  });

  networkMock.expects().send('/getBalance.jsp', TypeOf.isA(Function));

  var slotMachine = new drw.SlotMachine(null, null, null, null, networkMock);

  slotMachine.getBalance();

  mockControl.verify();

  }

  现在使用更少的代码就能得到相同的反馈,我们甚至打下了更简单的基础,有利于进一步测试。如何做到这样的呢?代码的第一行使用JsMock提供的 MockControl构造函数创建一个对象。代码于是创建了一个有send方法的mock对象。在一个有实际NetworkClient类的应用程序中,我们甚至不必把一个object literal应用到createMock方法中。JsMock可以通过原型推断出来。

  var mock = mockControl.createMock(NetworkClient.prototype);

  一旦network客户端的mock对象创建之后,我们编程期望带有特定参数的send方法被调用了一次。我们关心服务器资源名称是正确的,并且第二个参数是一个回调函数。mock对象被注入到被测系统的构造函数中,通过MockControl对象的验证方法,验证交互行为从而得出结论。如果因为任何原因,老虎机的实现没有调用network客户端的send方法,或者与预期的参数不符,验证方法会抛出一个异常,测试会失败。

  现在让我们编写另外一个测试,验证一个drw.SlogMachine实例什么时候、多久回到客户端一次。如果服务器端响应完成之前getBalance方法被调用,我们不希望余额被返回两次。这会导致老虎机的余额两次返回到用户账户,并且花费额外的带宽。

  function testGetBalanceWithMocksToTheNetworkOnce(){

  var mockControl = new MockControl();

  var networkMock = mockControl.createMock({

  send : function() {}

  });

  networkMock.expects().send('/getBalance.jsp', TypeOf.isA(Function));

  var slotMachine = new drw.SlotMachine(null, null, null, null, networkMock);

  slotMachine.getBalance();

  slotMachine.getBalance(); // no response from server yet

  slotMachine.getBalance(); // still no response

  mockControl.verify();

  }

  还记得我们在这里的第一个crack吗?当时我们创建了一个自己的微型mocking框架?那看上去像是一个实用的解决方案,但是你想像一下测试这样的交互行为,会写多少代码。仅仅由于参数的原因,让我们看看一个纯粹的stub解决方案,有多少瑕疵。

  function testGetBalanceFlawed(){

  var networkStub = {

  send : function() {

  if(this.called)

  throw new Error('This should not be called > 1 time');

  this.called = true;

  }

  };

  var slotMachine = new drw.SlotMachine(null, null, null, null, networkStub);

  slotMachine.getBalance();

  slotMachine.getBalance(); // no response from server yet

  slotMachine.getBalance(); // still no response

  }

  测试断言,网络客户端只被调用了一次,第一次使用之后网络stub就简单地抛出错误。这里有一个小问题,因为测试是人工控制待测对象断言。比如,如果待测系统将要多次调用网络stub的send函数,而它自己处理抛出的异常,那么测试永远也不会失败,因为test runner永远也不会收到任何出问题的通知。一个解决方法是创建更精致的微型mocking框架,但是通用的诸如JsMock这样的方法通常更简单。

  JsMock不仅仅能够让我们测试方法调用顺序和参数值。这个测试演示老虎机在网络发生故障时的行为。

  function testGetBalanceWithFailure(){

  var buttonStub = {};

  var mockControl = new MockControl();

  var networkMock = mockControl.createMock({

  send : function() {}

  });

  networkMock.expects()

  .send('/getBalance.jsp', TypeOf.isA(Function))

  .andThrow('network failure');

  var slotMachine = new drw.SlotMachine(buttonStub, null, null, null, networkMock);

  slotMachine.getBalance();

  assertEquals('Sorry, can't talk to the server right now', buttonStub.value);

  mockControl.verify();

  }

  这里我们验证老虎机在网络发生故障时可以优雅地失败。这是单元测试能够胜过系统集成测试的一个很好的例子。您能想像每个QA/发布周期中,对服务器每个集成点手工模拟一个网络故障花费的时间和金钱吗?

  getBalance方法的实现现在看上去是这样的:

  drw.SlotMachine.prototype.getBalance = function() {

  if(this.balanceRequested)

  return;

  try{

  // this line of code requires the very excellent functional.js

  // library, found at http://osteele.com/sources/javascript/functional

  this.networkClient.send('/getBalance.jsp', this.deposit.bind(this));

  this.balanceRequested = true;

  }catch(e){

  this.buttonElement.value = 'Sorry, can't talk to the server right now';

  }

  };

  与stub相比,mock的一个缺点是和被测系统的耦合相当多,至少希望拿来就用。当被测系统的行为与期望不符时,你希望测试失败──你并不希望被封装的实现细节有任何变化时,测试就会失败。为了弥补这种情况,JsMock提供了放宽期望的能力。其实你已经看到了这个例子。当我们准备网络mock对象时,这样写的:

  networkMock.expects().send('/getBalance.jsp', TypeOf.isA(Function));

  我们并没有指定哪个回调函数会被用作第二个参数,只需要是一个回调函数就可以了。如果我们想把这些期望放的更宽些,我们可以这样尝试:

  networkMock.expects().send(TypeOf.isA(String), TypeOf.isA(Function));

  如果我们想引用网络客户端mock的send方法的实际回调函数,我们可以使用JsMock框架的andStub方法:

  var depositCallback;

  networkMock.expects()

  .send('/getBalance.jsp', TypeOf.isA(Function))

  .andStub( function(){depositCallback = arguments[1];} );

  depositCallback({responseText:"10"});

  在我们继续之前,关于mock对象有两点需要知道。注意到每个测试最后如何调用MockControl的verify方法。这很重要。单元测试不调用 verify方法就不会失败。许多开发者遇到过这样的事情,写了一些标准的单元测试函数之后,就认为把对verify方法从测试函数移到tearDown 函数更好。虽然这会节省几行代码,也让你不必在每个测试函数的最后都去记住这一重要细节,不幸的是,它会给你带来一个新问题:tearDown中抛出的异常会被测试中抛出的第一个异常掩盖。第二个陷阱是新手经常过度使用mock对象,并用它们完全替代stub。不要这样。用stub来做基于状态的测试,使用mock做基于交互的测试。

0
相关文章