单页应用详解——(4)测试

来源:互联网 发布:百度输入法编程皮肤 编辑:程序博客网 时间:2024/05/17 06:21

单页应用详解(Single page apps in depth

4.测试

本文为译文,原文:http://singlepageappbook.com/maintainability3.html


TDD?让代码可以测试最好的方式就是编写代码时要与first-TDD风格,TDD归结为:


TDD是一系列编写代码的规则:写一个失败的测试(red),然后用足够的代码让它通过(green),然后重构需要重构的地方。

在这章中,我们将讨论如何用Mocha来为项目建立测试,如何依赖注入你的CommonJS模块,如何测试一段异步的代码。如果没有了解过TDD,可以看看Kent Beck's book , Michael Feather's book


为什么要写测试?

测试驱动的开发不值得是因为它会catche错误,但是它能改变你思考模块之间的接口。编写代码前写测试会影响你考虑模块的接口,和模块间的耦合。它提供了一种安全的推动重构的方式,而且记录下了系统的预期行为。

大多数情况下,你没有完全理解系统就开始写。写出来的只是一个草稿。你想改进代码又不影响现有的代码。这就是测试的目的:告诉你重构的时候哪些预期需要执行。

测试什么?

测试驱动开发以为这测试应该引导开发。在开发一个新功能的时候,我通常把测试当成todo,在不知道代码会在测试的时候会是什么样之前是不写代码的。测试就是合同:这是模块需要给外部提供的一部分。

我发现测试的最大价值来自于纯粹的逻辑测试和其它边缘情况下的案例。我趋向于不测试内部细节。我也避免被测试的东西很难建立测试,测试是一个工具,而不是一个目标。views,我一般测试逻辑,并让它可以独立测试不依赖于显式属性。

测试框架

除了Jasmine之外,任何测试框架都是可以的。Jasmine异步测试非常糟糕,它需要很多样板代码( amount of boilerplate code)。

通常有3种测试风格:

  • BDD:describe(foo) .. before() .. it()
  • TDD:suite(foo) .. sexports['suite'] = { before: f() .. 'foo should': f() }etup() .. test(bar)
  • exports:exports['suite'] = { before: f() .. 'foo should': f() }
我喜欢TJ's Mocha,有很多很棒的功能(a lot of awesome features).比如,支持上面三种风格,支持在浏览器运行测试,代码覆盖,Growl集成,文档生成,飞机模型和一个nyan cat测试reporter。我喜欢”exports“风格,简单。

有些框架需要使用它们的assert()方法,Mocha不需要。我使用Node's内置assert模块来写断言。我不是一个"assertions-written-out-as-sentences"风格崇拜者,简单的断言对我来说更有可读性,因为它直观的代表了代码。and it's not like some non-coder is going to go poke around in your test suite。

建立并写一个测试
让我们用mocha 来建立一个Node项目并写一个测试,创建项目目录,初始化package.json,安装mocha:
[~] mkdir example[~] cd example[example] npm initPackage name: (example)Description: Example systemPackage version: (0.0.0)Project homepage: (none)Project git repository: (none)...[example] npm install --save-dev mocha
我喜欢exports的测试风格:
var assert = require('assert'),    Model = require('../lib/model.js');exports['can check whether a key is set'] = function(done) {  var model = new Model();  assert.ok(!model.has('foo'));  model.set('foo', 'bar');  assert.ok(model.has('foo'));  done();};

注意done()方法。要调用这个方法通知测试程序这个测试已经完成。这让一步测试非常简单,你可以在一步方法调用最后调用done()。

可以使用before/after and beforeEach/afterEach,来定义测试开始前,后的代码。
exports['given a foo'] = {  before: function(done) {    this.foo = new Foo().connect();    done();  },  after: function(done) {    this.foo.disconnect();    done();  },  'can check whether a key is set': function() {    // ...  }};
还可以建立嵌套测试:
exports['given a foo'] = {  beforeEach: function(done) {    // ...  },  'when bar is set': {    beforeEach: function(done) {      // ...    },    'can execute baz': function(done) {      // ...    }  }};

基本的断言
  • assert.ok(value, [message])
  • assert.equal(actual, expected, [message])
  • assert.deepEqual(actual, expected, [message])
更多,可参考文档the assert module documentation 

测试要容易运行
编写了一个makefile来运行真个测试
TESTS += test/model.test.jstest:  @./node_modules/.bin/mocha \    --ui exports \    --reporter list \    --slow 2000ms \    --bail \    $(TESTS).PHONY: test
执行”make test“就可以运行了。
我还喜欢把失败的测试通过  via node ./path/to/test.js来运行,通过下面代码包装下,判断下是否是main script,如果是就直接运行测试。
// if this module is the script being run, then run the tests:if (module == require.main) {  var mocha = require('child_process').spawn('mocha', [ '--colors', '--ui',  'exports', '--reporter', 'spec', __filename ]);  mocha.stdout.pipe(process.stdout);  mocha.stderr.pipe(process.stderr);}



测试模块之间的交互

单元测试只能在一次测试一个模块。每个单元测试执行一个模块的测试。直接输入(比如方法的参数)传入到模块。只要值被返回,断言就会核查直接输出。

但是,更复杂的模块可能会用到其它模块,比如通过函数读取数据库,写入数据库。


如果想把依赖(比如数据库模块)换成另一个更早用于测试为目的的东西,有几种好处。

  • 可以捕获直接输出和控制输入。
  • 可以模拟错误条件,比如timeout,练级错误。
  • 可以避免比较难建立外部依赖,比如数据库和外部API。
这就是依赖注入。简单的例子,可以把一个依赖的function替换成一个假的。:
exports['it should be called'] = function(done) {  var called = false,      old = Foo.doIt;  Foo.doIt = function(callback) {    called = true;    callback('hello world');  };  // Assume Bar calls Foo.doIt  Bar.baz(function(result)) {    console.log(result);    assert.ok(called);    done();  });};

在跟复杂的案例中,如果你想替换整个后端的对象。有2种方案:构造参数和模块替换

构造参数

用配置来通过依赖:

function Channel(options) {  this.backend = options.backend || require('persistence');};Channel.prototype.publish = function(message) {  this.backend.send(message);};module.exports = Channel;
比如上面这段代码,写测试的时候,用一个测试对象来替代真是对象传进去。

var MockPersistence = require('mock_persistence'),    Channel = require('./channel');var c = new Channel({ backend: MockPersistence });


当然,这种做法不是很理想

代码会更杂乱,必须写this.backend.send而不能写Persistence.send,而且仅当测试的时候才传入这个option


模块替换

另一种方法是写一个function改变模块中依赖的值,比如:

var Persistence = require('persistence');function Channel() { };Channel.prototype.publish = function(message) {  Persistence.send(message);};Channel._setBackend = function(backend) {  Persistence = backend;};module.exports = Channel;
_setBackend方法就是用来替换内部模块Persistence 。
写测试的时候用require()一个模块,然后调用setBackend()来注入依赖的模块。

// using in testvar MockPersistence = require('mock_persistence'),    Channel = require('./channel');exports['given foo'] = {  before: function(done) {    // inject dependency    Channel._setBackend(MockPersistence);  },  after: function(done) {    Channel._setBackend(require('persistence'));  },  // ...}var c = new Channel();

还有一些其它的技术,包括创建工厂类(这会让common部分变得更复杂),重定义require(可以使用Node's VM  API)。但我更喜欢上面的方式,我有一种更抽象的方法,但事实证明不值得那么做,_setBackend()就是解决这个问题最简单的方式。


测试异步代码

有3种方式:

  • 写一个工作流
  • 等待事件,当预期条件完成了继续
  • 记录事件并断言
写工作流最简单的情况是:有一系列的操作需要触发,在测试中建立一些callbacks(可能是用callbacks替换掉一些函数)。在callback链的最后调用done()。可能还会加一个断言计数器来通知所有的callbacks都触发了。

这里有一个简单的workflow示例,注意每一步流程调都有一个callback。
exports['can read a status'] = function(done) {  var client = this.client;  client.status('item/21').get(function(value) {    assert.deepEqual(value, []);    client.status('item/21').set('bar', function() {      client.status('item/21').get(function(message) {        assert.deepEqual(message.value, [ 'bar' ]);        done();      });    });  });};


使用EventEmitter.when()等待事件
在某些情况下,无法清晰的定义事情发生的顺序。通常接口是一个EventEmitter。EventEmitter是Node里的一个名称,其实就是一个事件集合,在其它js项目中也会出现,比如jQuery用.bind()/.trigger()做同样的事情。

 Node.js EventEmitterjQueryAttach a callback to an event.on(event, callback) / .addListener(event, callback).bind(eventType, handler) (1.0) / .on(event, callback) (1.7)Trigger an event.emit(event, data, ...).trigger(event, data, ...)Remove a callback.removeListener(event, callback).unbind(event, callback) / .off(event, callback)Add a callback that is triggered once, then removed.once(event, callback).one(event, callback)当然jquery会有些选择器的功能。在测试的时候如果事件的触发不是在一个定义的顺序的话用通常的EventEmitter API就显得有点尴尬。

  • 如果用EE.once(),必须手动处理misses和手动计算。
  • 如果使用EE.on(),必须在测试的最后手动detach,而且需要更复杂的计算。
EventEmitter.when()是对标准EventEmitter API的一个轻量扩展:
EventEmitter.when = function(event, callback) {  var self = this;  function check() {    if(callback.apply(this, arguments)) {      self.removeListener(event, check);    }  }  check.listener = callback;  self.on(event, check);  return this;};
基本功EE.once()差不多,接受一个事件和回调。不同的是,callback的返回值依赖于这个callback是否removed。
exports['can subscribe'] = function(done) {  var client = this.client;  this.backend.when('subscribe', function(client, msg) {    var match = (msg.op == 'subscribe' && msg.to == 'foo');    if (match) {      assert.equal('subscribe', msg.op);      assert.equal('foo', msg.to);      done();    }    return match;  });  client.connect();  client.subscribe('foo');};


记录事件然后断言
当要为依赖写一个完整的替换代码变得不可行时,或者当收集输出更方便的时候,记录依赖会用的更频繁,然后断言是否满足条件。
比如,有一个EventEmitter,我们并不关心消息的顺序是如何触发的,只要触发就可以了。
exports['doIt sends a b c'] = function(done) {  var received = [];  client.on('foo', function(msg) {    received.push(msg);  });  client.doIt();  assert.ok(received.some(function(result) { return result == 'a'; }));  assert.ok(received.some(function(result) { return result == 'b'; }));  assert.ok(received.some(function(result) { return result == 'c'; }));  done();};

DOM或者一些难以模拟的依赖,就可以把我们调用的函数替换成另外一个。
exports['doIt sends a b c'] = function(done) {  var received = [],      old = jQuery.foo;  jQuery.foo = function() {    received.push(arguments);    old.apply(this, Array.prototype.slice(arguments));  });  jQuery.doIt();  assert.ok(received.some(function(result) { return result[1] == 'a'; }));  assert.ok(received.some(function(result) { return result[1] == 'b'; }));  done();};
只要替换成一个函数,然后在函数里调用原来的函数。

扩展阅读:
C. Johansen's blog and book
http://channel9.msdn.com/events/mix/mix11/EXT23