怎样写单元测试

最近和同事讨论过几次如何写单元测试之后,突然意识到,是要写点什么了。
什么是单元测试, 维基百科上是这么定义的: unit testing is a method by which individual units of source code, sets of one or more computer program modules together with associated control data, usage procedures, and operating procedures, are tested to determine if they are fit for use.[1] Intuitively, one can view a unit as the smallest testable part of an application. 简而言之,就是验证系统中最小可测试单元的功能是否正确的自动化测试。因此,单元测试的目地就是“对被测试对象的职责进行验证”, 在写单元测试之前,先识别出被测试对象的职责,就知道该怎么写这个单元测试了。

在我看来,根据被测试对象,单元测试可以分为两大类:“对不依赖于外部资源的组件的单元测试”和“对依赖于外部资源的组件的单元测试”。由于测试的对象的不同,我们需要采用不同的测试方案:

对不依赖于外部资源的组件的单元测试

对不依赖于外部资源的组件进行测试时,我们主要关注被测试对象的状态变化是否和预期的一致,而对于其内部实现,则不用关心,举个例子:

Account account = new Account(20)
assertThat( account.withdraw(10).balance(),  is(10))

开始账户里有20元,扣款10元后余额应该是10元,我们不关心扣款是现金交易还是银行转账,我们只关心扣款功能是正常的, 这就是一种黑盒测试

对依赖于外部资源的组件的单元测试

对依赖于第三方库、组件的接口的组件进行单元测试时,需要对依赖的库进行mock,让依赖库按照约定表现不同的行为,并且,在测试中验证被测试代码是否按照约定正确的调用了第三方接口(是否调用了正确的API, 是否传递了正确的参数), 我们再来看个例子:

//AuthenticateService是一个外部部件通过Web Service暴露的用户鉴权接口,
//在单元测试中,我们使用一个mock的AuthenticateService对象,来验证UserServiceImpl中的接口调用是否正确。
AuthenticationService mockAuthService = createMock(AuthenticateService.class)
UserService userService = new UserServiceImpl(mockAuthService);
User user = new User("nicholasren", "encrypted_password");

assertThat(userService.authenticate(user), is(true));

//期望userService.authenticate中会调用AuthenticateService.authenticate, 并且传递了正确的用户名和加密后的密码
verify(mockAuthService).authenticate(user.getName(), user.getPassword())

上面的例子中,对于依赖于外部资源的组件,我们需要验证的是该组件调用了正确的第三方接口,并且传入了正确的参数,这就是一种白盒测试

对依赖于框架的组件的单元测试

我把这种情形也归类为“依赖于外部资源的组件的单元测试”,但是特殊的是,被测试对象依赖的是框架,而这个框架代码在单元测试运行是不会被执行,我们来看个例子:

  //load.js
  $('.trigger').click(loadItems)

  //...
  loadItems:function() {
    var ajaxLoading = $("<li class='ajax-loading' style='display:block;'>Loading...</li>");
    var itemContainer = $(".container");
    $.ajax({
      url: this.config.itemLoadingUrl + groupID,
      type: 'GET',
      beforeSend: function(){
        itemContainer.append(ajaxLoading);
      },
      complete: function(){
        ajaxLoading.remove();
      },
      success: function (data) {
        itemContainer.append(data);
      },
      error: function () {
        itemContainer.append("<li class='error' style='display:block;'>Oops, connection time out. Please try again later.</li>");
      }
    });
  }

$.ajax 是jquery提过的发送ajax请求的方法,然而,在spec中,我们无法真正地发送ajax请求,然后再进行验证,于是我们stub了这个方法。 这时候,就有个问题了:
真正的$.ajax()方法没有被调用,complete,success,error的这几个function都没有被调用,那么这些function中的逻辑该如何测试呢?

$.ajax是jquery提供的API,出现错误的机率是非常非常低的,因此,我们可以假设$.ajax是正常工作的,然后stub $.ajax, 当被测试代码中调用$.ajax时,让其调用我们stub的一个fake function. 例如,我们想测试ajax request失败时,是否正确地显示了错误信息,那么我们可以写出下面的测试:

  //load_spec.js
  it("should show error message when ajax request failed", function(){
    spyOn($, "ajax").andCallFake(function(params) {
      params.beforeSend();
      params.complete();
      params.error("", "404");
    });
  
    var trigger = $(".itemContainer .trigger");
    trigger.click();
  
    expect($(".itemContainer  .error").length).toBe(1);
    expect($(".ajax-loading").length).toBe(0);
  });

$.ajax 是jquery提过的发送ajax请求的方法,然而,在spec中,我们无法真正地发送ajax请求,然后再进行验证,于是我们stub了这个方法。 这时候,就有个问题了:
真正的$.ajax()方法没有被调用,complete,success,error的这几个function都没有被调用,那么这些function中的逻辑该如何测试呢?

$.ajax是jquery提供的API,出现错误的机率是非常非常低的,因此,我们可以假设$.ajax是正常工作的,我们可以stub $.ajax,传入了一个fake function,在这个fake function中执行我们期望的行为,然后验证这些function的执行效果。

使用jasmine的spyOn机制,传入一个fake function,在此function中依次执行 beforeSend, complete,error 这几个callback,(注意上面andCallFake里的三行,这也是真实情况下ajax 请求失败时,这些callback被执行的顺序),然后验证error这个callback是否显示了error message。

因此,对于依赖于外部API的组件的测试,当此API在单元测试中不能被执行,我们可以假设其API工作正常,通过测试代码模拟其API执行的情况, 验证被测试对象的行为

一句话,写单元测试之前,弄清被测试对象的职责,然后针对被测试对象的职责进行测试,单元测试写起来,真的不难。

How SSH works?

简介:

SSH,全名secure shell,其目的是用来从终端与远程机器交互,SSH设计之处初,遵循了如下原则:

  • 机器之间通讯的内容必须经过加密。
  • 加密过程中,通过 public key加密,private 解密。

密钥:

SSH通讯的双方各自持有一个公钥私钥对,公钥对对方是可见的,私钥仅持有者可见,你可以通过”ssh-keygen”生成自己的公私钥,默认情况下,公私钥的存放路径如下:

  • 公钥:$HOME/.ssh/id_rsa.pub
  • 私钥:$HOME/.ssh/id_rsa

通讯原理:

前提条件:

  1. 两个节点都持有各自的公钥私钥对,分别标记为PUBLIC KEY(client), PRIVATE KEY(client), PUBLIC KEY(server), PRIVATE KEY(server)
  2. 服务器上运行了SSH服务程序

建立通信通道的步骤如下:

  1. 客户端发起请求给服务器,服务器发回自己的public key给客户端
  2. 客户端检查这个public key是否在自己的$HOME/.ssh/known_hosts中,如果没有,客户端会提示是否要把这个public key加入到known_hosts中。
  3. 客户端会提示输入密码,用户输入密码后,客户端会使用PUBLIC KEY(server)对密码加密,然后发送给服务器。
  4. 服务器收到密码后,使用PRIVATE KEY(server) 解密,校验密码正确性. ??? 需要解密吗?
  5. 客户端把PUBLIC KEY(client), 发送给服务器。
  6. 至此,通讯通道建立完毕,当客户端想服务器发送消息时,会使用PUBLIC KEY(server)加密,服务器会使用PRIVATE KEY(server)解密,当服务器向客户端发送消息时,会使用PUBLIC KEY(client)加密,客户端收到数据后,会使用PRIVATE KEY(client)解密。

免密码登录:

我们的目标是: 用户已经在主机A上登录为a用户,现在需要以不输入密码的方式以用户b登录到主机B上。
步骤如下:

  1. 以用户a登录到主机A上,生成一对公私钥。
  2. 把主机A的公钥复制到主机B的authorized_keys中,可能需要输入b@B的密码。

    ssh-copy-id -i ~/.ssh/id_dsa.pub b@B

  3. 在a@A上以免密码方式登录到b@B
ssh b@B

SSH forwarding:

ssh -f user@ssh_host -L 1433:target_server:1433 -N

Getting started with chef

Set up a ubuntu node and run recipe on it

Predication:

  1. a running ubuntu node which accessiable via SSH from your labtop(workstation)
  2. cd to chef-repo directory.

    Steps:


    Bootstrap ububtu:
knife bootstrap IP_ADDRESS -x USERNAME -P PASSWORD --sudo


Verify the installation completed
knife client list

you will see your nodename in the client list

Download cookbooks from community site
knife cookbook site install getting-started

the cookbook named “getting-started” will be downloaded into chef-repo/cookbooks/

Upload the recipe to Hosted Chef so it is available for our nodes
knife cookbook upload getting-started 


Add this new recipe to the new nodes run list
knife node run_list add NODENAME 'recipe[getting-started]'


Run the added recipe remotely via ssh
knife ssh name:NODENAME -x USERNAME -P PASSWORD "sudo chef-client" -a ipaddress


Runnig chef-client as a deamon
knife cookbook site install chef-client
knife cookbook upload chef-client
knife node run_list add NODENAME 'recipe[chef-client]'
knife ssh name:NODENAME -x USERNAME -P PASSWORD "sudo chef-client" -a ipaddress


Advanced tips:

  • add recipe when bootstrap node
knife bootstrap IP_ADDRESS -r 'recipe[chef-client]' -x USERNAME -P PASSWORD --sudo

Polymorphic Association

有这么一个需求,一个在线音乐商店系统,我们暂且叫它’online-store’,需要记录消费者对每首歌的评论,一个有经验的rails developer会很快地写出下面的代码:

class Music
  has_many :comments
end

class Comment
  belongs_to :music
end

对应的表结构如下:

#musics:   
  |  id  | integer|   
  | name | varchar|   
  | ...  |  ...   |   
#comments:   
  | id      | integer|   
  | text    | varchar|   
  | music_id| integer|   

如果需要给music添加,查看评论,可以通过如下代码实现:

  #添加评论
  music.comments.create {:text => 'this is realy nice'}
  #查看评论
  music.coments

风云变幻,斗转星移,apple的app store创造了软件销售的新模式,我们的vendor也坐不住了,决定在现有的音乐商店系统上出售应用程序,电影,游戏等内容,同样,这些内容也需要支持评论,有了前面成功的经验,你信心满满增加了下面几个model:

  class Application
    has_many :comments
  end

  class Movie
    has_many :comments
  end

  class Game
    has_many :comments
  end

再来看看我们之前写的model Comment:

class Comment
  belongs_to :music
end

现在需要支持多种内容,而且这些类内容之间出了都可以被评论外,再无其他关联,那这个belongs_to该怎么写呢?一个最直接的思路是,扩展Comment,让其支持对以上四类内容的评论,代码如下:

class Comment
  belongs_to :music, though => "music_id"
  belongs_to :game, though => "game_id"
  belongs_to :application, though => "application_id"
  belongs_to :movie, though => "movie_id"
end

表结构如下:

#comments:   
  | id             | integer|   
  | text           | varchar|   
  | music_id       | integer|   
  | game_id        | integer|
  | application_id | integer|
  | movie_id       | integer|

有了以上的model,你就可以给应用程序,电影,游戏增加评论了:

  #创建评论
  application.comments.create {:text => "this is a nice app"}
  movie.comments.create {:text => "this is a nice movie"}
  game.comments.create {:text => "this is a nice game"}
  #查看评论
  application.comments
  movie.comments
  game.comments

目前看来,这些代码工作得很好,然而,做为一个有着良好直觉的程序员,你敏锐地觉察到,将来可能有更多的内容出现在这个”onlne-store”中,也会有更多的内容需要支持评论——你成功地识别出一个”易变点“,每当新增一种内容的时候,你就需要打开这个Comment类,新增一个association,同时,还需要增加migration,这个设计明显违背了开闭原则”。那么,这个问题该怎么解决呢? 再来分析下这个问题,上面我们提到,这些model唯一的共性是 “可以被评论”,于是我们可以抽象出一个概念——“commentable”。如果我们让comment对象知道它所对应的”commentable”对象的id以及类型(game/application/movie),我们就可以获得一个“commentable”对象的所有comments,参考下面的代码:

  #查看id为1的music的评论
  Comment.find(:commentable_id => 1, :commentable_type => 'music')

  #查看id为1的application的评论
  Comment.find(:commentable_id => 1, :commentable_type => 'application')

如此,comments的表结构就可以简化为:

#comments:   
  | id              | integer|   
  | text            | varchar|   
  | commentable_id  | integer|   
  | commentable_type| varchar|

model代码简化为:

 #添加评论
 Comment.create({:text => "good staff", :commentable_id => "1", :commentable_type => "music"})
 #查看评论
 Comment.find(:commentable_id => 1, :commentable_type => 'music')
  class Comment
    belongs_to :commentable, :polymorphic => true
  end
  class Application
    has_many :comments, :as => :commentable
  end
  class Movie
    has_many :comments, :as => :commentable
  end
  class Game
    has_many :comments, :as => :commentable
  end
  #添加评论
  movie.comments.create {:text => 'this is realy nice'} 
  #查看评论
  movie.coments

更多关于“polymorphic association”的信息,请参考这里 怎么样,这样一来,再新增多少种内容类型,处理起来都非常容易,扩展下commentable_type的值就可以了。这个思路的根本出发点在于为识别、分离变化点。这类问题可以抽象为这样一个问题:”如何把一个概念应用到一组除了这个概念,没有其他任何关联的对象上?” ,此类问题可以采用上述思路解决。

声明:本文翻译自这里

递归101

我们都知道什么是递归吧?一个自己调用自己的函数,或者函数A调用函数B,函数B又调用函数A,或者是A调用B,B调用C, C调用A。但是我们大多数情况下所说的递归函数,是指一个调用自身的函数。
在Java世界中,递归函数的曝光率很低,说起来有不少原因。
第一,递归不直观,难以理解。对于一段循环代码(for, while), 你可以很直白地看到这段逻辑的全景,即便你是一个初学者,而对于一段递归代码,就不是那么容易了,你只能看到递归逻辑的一次调用,而不得不想象当递归调用发生时,这些多次调用是如何组合在一起的。
第二,比起递归,循环在java中更加容易实现,比如for ,for-each,while, do-while, 数组, iterator, ResultSet,这些结构都是用来实现循环的。
第三,Java中的递归有自己的Achille’s heel: call stack。

总的来说,当调用一个函数时,一个新的call stack frame会被放到call stack的顶部,用以保存局部变量,当前函数的caller,等等。但是,call stack的大小是有限的,当递归的深度不是很深时,调用递归函数是没有什么问题的,但是如果递归调用的深度无法预计,那么很有可能会导致stack overflow. 而循环却不会有这种问题(因为循环不会产生新的call stack frame),因此,使用循环更加安全。

Scala中的递归

Scala,作为一新兴的functional language,更偏爱递归胜过循环,那么在scala中,是如何解决call stack大小限制的问题的呢?我们来看一个例子:

  def listLength1(list: List[_]): Int = {
    if (list == Nil) 0
    else 1 + listLength1(list.tail)
  }

  var list1 = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
  var list2 = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
  1 to 15 foreach( x => list2 = list2 ++ list2 )

  println( listLength1( list1 ) )
  println( listLength1( list2 ) )

函数listLenght1递归地计算list中item的数量,这个函数在计算item数量较少的list时工作地很好,然而在item数量很大时,就会得到stack overflow错误. 递归是函数式语言的谋生手段,但是在Scala中,递归仍然倒在了stack overflow的脚下。

先别急着放弃,Scala有一个很重要的优化递归的方案,只要你用了正确的递归类型。

首递归和尾递归

根据递归调用的方式,递归可以分为首递归和尾递归。在首递归函数中,函数调用自身后,再进行其他运算(可能会把自用自身后的结果做为这些运算的输入)。在尾递归函数中,所有的计算都在函数调用自身前完成,调用自身是尾递归函数中做的最后一件事情。

这两种递归的区别的重要性目前看起来还不是那么明显,然而,它确实很重要!想象一下一个尾递归函数的执行过程,首先完成所有的计算,在最后一步,马上进行对自身递归的调用,一般情况下,这个时候就该使用call stack frame记录方法调用状态了,然而,这里却不需要:我们不需要记录局部变量,因为所有的计算都已经完成。我们也不需要知道目前在哪个函数中,因为我们始终在同一个函数中。基于以上前提,scala不会创建一个新的call stack frame,而是重用当前的call stack frame ,无论调用次数有多少,call stack也不会增长。这就是scala中尾递归函数的特殊性。在其他语言中,语言设计者通过把尾递归转换成循环的方式进行了优化。

而在首递归函数中,递归调用是不一样的,这是为什么呢?想象一下一个首递归函数的执行过程:先执行一些计算,在递归调用自身,然后在执行另一些计算。在调用自身前,需要记住当前的局部变量,以便在从递归调用返回后继续进行后面的运算,这样,就必须创建一个新的call stack frame来记录当前状态。因此首递归函数还是会有stack overflow的风险。并且无法被优化。

在这里问你一个问题,上面的listLenght1是一个尾递归还是首递归?让我们来看着这个函数做了哪些事情。
A) 检查参数是否为Nil。
B) 如果为空,返回零,因为Nil的长度是零。
C) 如果不为空,则返回1加上递归调用的结果。
递归调用逻辑在这个函数的最后一行,应该是尾递归函数吧?错!在尾递归调用结束后,然后对递归调用结果加一,然后返回最终结果。这实际上首递归(或者可以叫做中递归)因为递归调用并不是所有运算的最后一步。

尾递归例子

当你用scala写一个递归函数时,你的目标是写成尾递归以便编译器对尾递归进行优化。现在让我们把上面的那个函数重写为尾递归函数。

def listLength2(list: List[_]): Int = {
  def listLength2Helper(list: List[_], len: Int): Int = {
    if (list == Nil) len
    else listLength2Helper(list.tail, len + 1)
  }
  listLength2Helper(list, 0)
}

println( listLength2( list1 ) )
println( listLength2( list2 ) )

我写成两个函数(listLength2 和一个内部的helper函数)以便和上面的例子中的函数接口保持一致。如果你能给listLength2Helper的参数给个默认值,我们就能只提供一个函数,但是我不知道怎么做。长话短说:listLength2只调用了做了实际工作的listLength2Helper,而且listLength2Helper也是个递归函数。

listLength2Helper是个尾递归函数吗?递归调用是所有运算的最后一步,允许scala进行优化?就像listLenght1一样,listLength2首先检查参数是否为Nil,如果不是,就进行递归调用,但是仍然会有一个加一的操作 —— len + 1。难道这个就不是尾递归吗?不,len + 1 运算会在递归调用前运算。只有所有的参数运算完了以后,才会进行递归调用,这个函数确实是个尾递归函数。