Freewind @ Thoughtworks scala java javascript dart 工具 编程实践 月结 math python english [comments admin] [feed]

(2013-01-01) 33. 单元测试

广告: 云梯:翻墙vpn (省10元) 土行孙:科研用户翻墙http proxy (有优惠)

JavaScript是一门有强大表现力的动态类型语言,但它几乎无法从compiler中得到任何帮助。因此我们强调任何用JavaScript写的代码都需要有一组测试方法。我们在Angular中添加很多特性让你测试程序更方便,所以没有理由不写测试。

说的全都是不要混合专注点(It is all about NOT mixing concerns,怎么翻)

单元测试(Unit Test),如同它的名字所说,是关于单独一块代码的测试。单元测试试图回答这个问题:我认为我的业务逻辑是正确的吗?比如,这个排序函数是否将list按正确的顺序进行了排序?为了能回答这样的问题,隔离代码很重要。因为当我们测试这种函数时,我们不想被强迫去考虑那些如DOM操作、使用XHR取数据等无关操作。

在一个项目中,我们通常很难只调用一个单独的函数。原因在于开发者们经常把关注者混合在了一起,以致于一块代码中把所有的事情都干了:从XHR中读取数据,排序,然后操作DOM。在Angular中我们尝试让你以简单的方式做正确的事情,所以我们为你的XHR提供了依赖注入(在测试中你可以使用mock)。我们进行了抽象,让你对model进行排序时,无需手动对DOM进行排序操作。所以最终,测试一个“对数据排序”的函数变得容易,因为我们可以在测试中简单地创建一些数据集,应用到该函数,最后验证model中的数据的顺序是正确的。测试代码不需要等待XHR,不需要创建任何DOM,也不需要验证你的函数正确地修改了DOM。

Angular始终把“可测试性”放在重要位置,但仍然需要你做正确的事情。我们试着让正确的事情变得简单,但Angular不是魔法,如果你没有注意这些,最后可能还是写出了无法测试的代码。

依赖注入

有几种方法可以取得一个依赖:

  1. 使用new操作符创建2. 从一个叫“全局单例”的地方寻找3. 从注册表(如service注册表)中寻找(但你从哪儿找到这个注册表呢?参看2)4. 你也可以期待有人把它递给你

在上面的例子中,只有最后一个是可测试的。让我们看看为什么:

使用new操作符

虽然从根本上讲new操作符没有错,但问题是使用对一个构造器使用new操作符,等于永久性地将类型与调用位置写死了。例如我们准备初始化一个XHR用来从server中取数据:

function MyClass() {
  this.doWork = function() {
    var xhr = new XHR();
    xhr.open(method, url, true);
    xhr.onreadystatechange = function() {...}
    xhr.send();
  }
}

问题在于,在测试中,我们非常希望初始化一个`MockXHR`来返回假数据和模板网络错误。而使用`new XHR()`,我们总是创建了一个真实的XHR,没有好办法来替换它。没错,虽然我们有`monkey patching`,但那从很多方面讲都是一个坏主意,当然这已经超出了本文的范围。

上面的这个类很难测试,因为我们必须求助于monkey patching:

var oldXHR = XHR;
XHR = function MockXHR() {};
var myClass = new MyClass();
myClass.doWork();
// assert that MockXHR got called with the right arguments
XHR = oldXHR; // if you forget this bad things will happen

##### 全局查找

另一种方法是在一个知名位置(全局单例)寻找service。

function MyClass() {
  this.doWork = function() {
    global.xhr({
      method:'...',
      url:'...',
      complete:function(response){ ... }
    })
  }
}

由于没有创建依赖的实例,从基本上讲它跟`new`是一样的,在测试中没有好办法拦截对`global.xhr`的调用,除非使用mongo patching。使用全局变量的基本问题是这些全局变量必须是可变的,可以被mock方法替换。关于更进一步的解释,参看[Brittle Global State & Singletons](http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons)。

上面的代码之所以难以被测试,是因为我们必须改变全局状态:

var oldXHR = global.xhr;
global.xhr = function mockXHR() {};
var myClass = new MyClass();
myClass.doWork();
// assert that mockXHR got called with the right arguments
global.xhr = oldXHR; // if you forget this bad things will happen

##### Service注册表

看起来我们可以对所有的services使用一个注册表,然后在测试中替换需要的service。

function MyClass() {
  var serviceRegistry = ????;
  this.doWork = function() {
    var xhr = serviceRegistry.get('xhr');
    xhr({
      method:'...',
      url:'...',
      complete:function(response){ ... }
    })
}

问题在于,我们从哪里去取得这个`serviceRegistry`?如果它是:

Directives

Angular中的Directives在model发生变化时,有责任更新DOM。(这段没写完)

(这段只有标题)

comments powered by Disqus