单页应用详解——(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() }
[~] 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()。
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])
TESTS += test/model.test.jstest: @./node_modules/.bin/mocha \ --ui exports \ --reporter list \ --slow 2000ms \ --bail \ $(TESTS).PHONY: test执行”make test“就可以运行了。
// 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。
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种方式:
- 写一个工作流
- 等待事件,当预期条件完成了继续
- 记录事件并断言
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(); }); }); });};
- 如果用EE.once(),必须手动处理misses和手动计算。
- 如果使用EE.on(),必须在测试的最后手动detach,而且需要更复杂的计算。
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');};
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();};只要替换成一个函数,然后在函数里调用原来的函数。
- 单页应用详解——(4)测试
- 单页应用详解——前言
- 单页应用详解——(5)创建视图
- 单页应用详解——(3)开始可维性
- 单页应用详解——(2)用模块化提高可维护性:停止使用命名空间
- 单页应用详解 ——(1)现代web应用,概述
- 单例模式——实战应用详解
- 详解JavaScript操作URL的方法(单页应用常用)
- 设计模式——单例(Unity3D中的应用)
- 七天学会ASP.NET MVC(七)——创建单页应用
- 七天学会ASP.NET MVC(七)——创建单页应用
- 七天学会ASP.NET MVC(七)——创建单页应用
- angularjs(一):单页应用
- 【SPA】单页Web应用(一)
- 【SPA】单页Web应用(二)
- 【SPA】单页Web应用(三)
- 单页Web应用
- WeX5--单页应用
- QT网上学习资源+查找文档
- 不去火车站也能买到火车票
- [C/C++标准库]_[初级]_[C++ iostream read getline 读取文件慢的原因]
- 重力感应器G—sensor 驱动分析
- S3C2440---点亮第一盏灯
- 单页应用详解——(4)测试
- myeclipse+8.6各个版本注册码
- Jlink更新至V4.65d后,Jlink不能使用的问题解决办法
- codeforces 245H 区间DP 好题
- 从此买票不去火车站
- 让控件可以处理自己的背景颜色
- 电话也可以买到火车票
- 省时省力买到火车票
- 【修身】喝水解决身体的“小毛病”