Note: 本文中的程序来自Martin Ordersky在www.coursea.org上所开设课程中的样例代码,文章主要目的是讲述我对此类问题解法及scala lazy evluation的理解。

问题描述

给你一个容量为9升的杯子和一个容量为4升的杯子,水不限使用,要求精确得到6升水。这就是“倒水问题”。我这里会讲述一个试用scala穷举法实现的一个例子。

建模

首先我们对这个问题进行建模。这个问题可以泛化为如下形式:

两个容量固定的杯子,可选的动作有:

  • 加满
  • 倒空
  • 从一个杯子倒入另一个杯子

初始状态为两个杯子都空,结束状态为其中一个杯子中的水量为所期望的结果。 从上面的描述中我们可以得到如下几个概念状态动作路径

状态用于表述各个杯子中当前的水量,动作用来改变各个杯子中水量,路径用于表示到达某一状态的动作序列。

状态

我们可以用一个Vector[Int]来表述杯子的状态,下标为杯子编号,元素值为当前杯子中的水量,另外,我们还需要一个Vector[Int]表示杯子的容量,下标为杯子编号,元素值为杯子的容量。于是我们得到如下代码:

class Pouring(capacity: Vector[Int]) {
  //States
  val initialState = capacity map (x => 0)
}

capacity为杯子容量, intialState为问题初始状态。

动作

由上可知,解决这个问题可以有三种动作, Empty, Fill, Pour,每个动作都会导致状态发生变化,于是我们得到如下代码:

//Moves
trait Move {
  def change(state: Vector[Int]): Vector[Int] 
}
case class Empty(glass: Int) extends Move {
  def change(state: Vector[Int]): Vector[Int] = state updated (glass, 0)
}
case class Fill(glass: Int) extends Move {
    def change(state: Vector[Int]): Vector[Int] = state updated (glass, capacity(glass))
}
case class Pour(from: Int, to: Int) extends Move {
  def change(state: Vector[Int]): Vector[Int] = {
      val amount = state(from) min (capacity(to) - state(to))
      state updated (from, state(from) - amount) updated (to, state(to) + amount)
  }
}

注意因为要考虑杯子的容量和杯子中剩余的水量,Pour中的change方法稍微复杂了些。

路径

接下来到了最重要的路径部分了,路径是到达某一状态的动作序列,我们需要一个List[Move]表述动作序列,一个Vector[Int]表述结束状态。 还需要一个extend方法来扩展当前的动作序列,即在当前路径上,应用一个动作(Fill, Empty, Pour),得到一个新的路径。于是我们得到如下代码:

//Paths
class Path(history: List[Move], val endState: Vector[Int]) {
  def extend(move: Move) = new Path(move :: history, move change endState)
  override def toString = (history.reverse mkString " ") + " -->" + endState
}

算法

这里我们要采用的是穷举法:

穷举从初始状态出发所有可能的动作,以及可能达到的状态,再穷举从这些状态出发所有可能的动作以及可能达到的状态,如此反复,直到找到一个可能达到的状态满足期望,则到达这个状态所经历的所有动作组成的路径即为问题的解。

首先我们来穷举给定一组杯子可能的动作:

val glasses = 0 until capacity.length
val moves =
  (for (g <- glasses) yield Empty(g)) ++
  (for (g <- glasses) yield Fill(g)) ++
  (for (from <- glasses; to <- glasses if from != to) yield Pour(from, to))

其次,在不进行任何动作时,动作列表为空,所达到的状态为初始状态,则:

val initialPath = new Path(List(), initialState)

接下来到最关键的部分了,穷举从初始状态出发的所有可能扩展出来的路径及其所达到的状态,由于从任何状态开始穷举,都会得到一个一组路径,而不是一个,于是我们首先定义一个从给定一组路径,穷举其可能扩展出来的的路径的方法:

def from(paths: Set[Path], explored: Set[Vector[Int]]): Stream[Set[Path]] = {
  if (paths.isEmpty) {
    Stream.empty
  } else {
    val more = for {
      path <- paths
      next <- moves map path.extend
      if !(explored contains next.endState)
    } yield next
    paths #:: from(more, explored ++ (more.map(_.endState)))
  }
}

paths为此次穷举的初始路径集合, explored用于记录已经穷举过的状态,以避免找出多条达到相同状态的路径,此方法通过穷举初始路径集合,在各个路径上扩展所有的动作,去掉那些达到状态已经被穷举过的路径,得到一组新的路径。

那么,从初始路径出发,其可能扩展出来的路径极其可能达到的状态如下:

val pathSets = from(Set(initialPath), Set(initialState))

对于给定的目标水量,遍历上述穷举结果路径找出endState包含目标水量的路径,如下:

  def solutions(target: Int): Stream[Path] = {
    for {
      pathSet <- pathSets
      path <- pathSet
      if path.endState contains target
    } yield path
  }

总结

我们知道,穷举是一个无穷无尽的过程,上面的程序是如何运行的呢?其实是scala中强大的lazy load发挥了作用。我们再来看from方法的返回值类型,是Stream[Set[Path]], Stream 用于表述元素序列,它的一个重要特点是,只有需要使用到其中的某个元素时,程序才会去计算这个元素。于是,在程序运行时,scala并不会一下子把所有可能的路径都计算出来。对于solutions方法,也是一样。因此,scala只有在从solutions的计算结果中获取满足条件的路径时,pathSet才会穷举可能的路径,并且在找到满足条件的路径后,计算会立即结束,不会再列举其余的可能。

注:感谢我的同事新宇想出的这个点子,没有他的这个点子,也就没有这篇文章。

引子

在开发过程中,你有没有碰到这样的场景:
“功能完成,该提交代码了。”
“Hold on,得先跑下local build。”,你的pair叫起来了。
“啊,是啊”,你恍然大悟。敲下bundle exec rake dev
“local build一般至少得运行十分钟吧, 抽空看看邮件吧”。
5分钟过去了,你回过神来,看看测试跑得怎么样了。
啊,测试在第二分钟时就悄无声息地废了。白白浪费了3分钟时间。。。。
“要是测试在废掉的时候能够提醒一下我就好了。”你心想。

做为一名自诩动手能力很强的程序员,有什么理由不自己试一试呢?

要达到上述目标,我们需要解决以下几个问题:

  1. 如何判断local build是否成功?
  2. 如何播放声音提示?

我们首先来看第一个问题,bundle exec rake dev实际上是在执行一个shell命令,如何判断一个shell命令是否执行成功, google之,得知可以通过退出状态码(Exit Code)进行判断,非零退出状态码表示shell脚本执行失败。于是我们创建一个脚本run_test.sh,脚本内容如下:

	#!/bin/bash -e
	bundle exec rake dev
	if [ $? == 0 ]; then
		#TODO 播放成功声音
	else
		#TODO 播放失败声音
	fi

接下来我们来看第二个问题,Mac OSX自带了一个命令行工具say,可以把单词转换为语音输出,例如执行say "Hello World",你就会听到Mac 说话了。 于是,我们的脚本就变成了:

	#!/bin/bash -e
	bundle exec rake dev
	if [ $? == 0 ]; then
		say "Good, build passed"
	else
		say "Boom, build failed"
	fi

俩问题都解决了,该伸个懒腰休息休息了吧。“Hold on”, 你的pair又说话了,“难道这么好用的功能,你只想用它来执行local build吗?能否让所有的rake task失败都有声音提示呢?” 。真是个难缠的pair。

不管怎样,第三个问题来了,如何使run_test.sh变得更加通用? 把要被执行的命令做为参数传给这段脚本,然后在脚本中执行,恩,是个不错的选择。于是脚本变成如下的样子:

	#!/bin/bash -e
	$@
	if [ $? == 0 ]; then
		say "Good, build passed"
	else
		say "Boom, build failed"
	fi

执行sh ./run_test.sh bundle exec rake dev,当bundle exec rake dev执行成功时,你就可以听到悦耳的声音提示了。

还能再改进吗?每次都要敲这么长的命令,好烦啊。加个alias吧,编辑/.rvmrc,加入如下alias:

	alias be="sh ./script/run_test.sh bundle exec"

下次只要执行be rake dev,你就可以可以放心地看邮件了,等着听声音提示吧。

后记

mac也提供了另外一个命令行工具afplay,可以用来播放音频文件,我们选用了卡巴斯基那个惨绝人寰的杀猪声做为build失败提示音,够惊悚吧。下面是我的项目中这个脚本的最终版本。

	#!/bin/bash -e
	$@
	if [ $? == 0 ]; then
	  afplay fixture/success.wav
	else
	  afplay fixture/fail.wav
	fi

想说的话

上面的故事是从今天我和同事pair的场景里抽象提炼出来的,从提出点子到最后实现也就用来个把小时,代码量很小,难度也不大,然而带给每个team member的好处却是显而易见的。让平日里的工作变得有趣的正是这些点子,以及实现它的过程,更大的乐趣是把它分享给更多的人。我们已经分享了,你呢?

本文发表于infoq

什么是BDD?

BDD在wikipedia上定义如下:

BDD是第二代的、由外及内的、基于拉(pull)的、多方利益相关者的(stakeholder)、多种可扩展的、高自动化的敏捷方法。它描述了一个交互循环,可以具有带有良好定义的输出(即工作中交付的结果):已测试过的软件。

简单一点地说,BDD,即行为驱动开发,是通过与产品经理沟通需求,定义出满足这些需求的软件需具备的行为(Behaviour),再以这些行为为驱动(Driven),编写产品代码来实现这些行为。(Development)。BDD的出现,是为了解决测试驱动开发中常遇到的问题,比如:从哪里开始测试,应该测试什么,不应该测试什么,等等。想了解更多可参见Dan North的introducing BDD。

BDD实践所面临的问题

进行BDD实践首先要解决如下几个问题:

  • 如何实现一个能够描述系统行为(业务价值)、非技术人员可读的测试?
  • 如何让这个测试变得可执行?

业界对这些问题已经有了答案,JBehave, CucumberConcordian等BDD框架的出现,解决了这个问题。 这些BDD框架各自提供了一套DSL(Domain-Specific-Language),开发人员可以使用DSL描述业务需求,例如,

前置条件: 用户A账户余额1000元 用户B账户余额200元
场景:
	用户A登录系统
	向用户B转账500元
	用户A账户余额应为500元
	用户B庄户余额应为700元

同时,这些框架都依赖于Webdriver(如selenium-webdriver,watir-webdriver),BDD框架通过webdriver调用浏览器的接口,模拟用户输入,读取浏览器页面上显示的内容用于验证。

下面我们通过一个完整的例子来看看如何使用这些工具进行BDD实践的。

Cucumber与业务价值

在Behaviour Driven Development中,第一步就是把需求细分为多个任务,拿最常见的用户登录功能为例,可以划分为以下几个任务:

  • 用户名密码匹配,登录成功
  • 用户名或密码不匹配,登录失败

BDD强调“每一个测试需要体现出业务价值”,因此,可以把上述两个任务实现为两个场景:

Feature: User login
Background: There is a user with the following login detail:
	|    email      | password|
	| my@example.com|   test  |
	
Scenario: Login succeed
Given the user login with the following detail:
	|    email      | password|
	| my@example.com|   test  |
Then the user should login succeed

Scenario: Login failed
Given the user login with the following detail:
	|    email      |     password     |
	| my@example.com|   wrongpassword  |
Then the user should login failed 实际上,上面的这段代码就是使用cucumber的DSL描述的测试场景,几乎就是遵循了一定格式的英语,即使看不懂代码的产品经理、业务分析师也能够通过此文档和开发人员顺畅地交流。用Cucumber把一个需求的不同场景描述出来,也是从不同角度阐述了这个需求的业务价值。__Cucumber的目标就是书写可执行的,能够表述业务价值文档。__ 与之类似的框架还有Concordian,JBehave等。

紧接而来的问题是:如何让文档执行起来?Cucumber提供了把业务逻辑转换为可执行代码的机制——”step definition”。请看下面的例子:

Given /^the user login with the following detail:$/ do |detail|
	#omitting code…
end 这个step definition会匹配下面这个step:

Given the user login with the following detail:
	|    email      | password|
	| my@example.com|   test  |

当Cucumber feature被执行的时候,这个step definition中的代码会被执行。那么,接下来的问题就是:如何象真实用户那样打开浏览器,输入用户名密码,点击提交按钮,验证登录是否成功。这时候,该Webdriver出场了。

Web Driver与页面交互

先来看下面一段代码:

require 'watir-webdriver'
b = Watir::Browser.new
b.goto 'http://localhost:3000/login'
b.text_field(:id => 'email').set 'my@example.com'
b.text_field(:id => 'password').set 'password'
b.button(:name => 'submit').click
b.text.include? 'Login succeed'

这段代码会做如下事情:

1. 打开浏览器,访问h地址 “http://localhost:3000/login”
2. 在邮件输入框输入 “my@example.com”
3. 在密码输入框输入 “password”
4. 点击 提交按钮
5. 验证结果页面是否包含“Login succeed”字样 这就是webdriver所提供的能力,web driver通过调用浏览器的支持自动化的API,模拟真实用户在浏览器上的操作。把这段代码被放在上面的step definition中,当cucumber测试运行的时候,这段代码就会运行,完成登录操作。这个例子是使用[Watir webdriver](http://watirwebdriver.com/)实现的,另外一个比较流行的webdriver是[Selenium webdriver](http://seleniumhq.org/projects/webdriver/)。

不同Webdriver提供的API也不尽相同,而Capybara则致力于封装多种web driver之间的差异。同时,Capybara提供了一些更聪明的特性,例如,等待页面加载完成再执行下一个步骤,这对于开发人员来说非常重要,否则,就需要自己判断写代码页面加载完成,代码丑陋,测试脆弱,那将是开发人员的噩梦。

Page Model与页面建模

至此,一个可执行的描述用户登录的测试用例就编写完毕,当我们执行这个测试用例时,就会看到:

浏览器打开
访问登录页面
在页面上输入用户名
密码
点击登录按钮
登录成功
测试通过 上述所有操作都是自动完成,一切都很完美,但前提是只在这样的一个小示例里。在一个实际的项目里,我们经常会遇到下面几个问题:

1.当越来越多的与页面交互的代码出现在step definition中时,页面交互,结果验证的代码混杂在一起,代码的可读性急剧下降。
2.因为webdriver与浏览器交互时依赖于页面元素的id、name等属性,对页面元素的任何小的修改都可能会导致测试失败。
3.在多个step definition与同一个页面交互时,可能会有冗余代码。

page model的出现就是为了解决上述问题,通过对页面的属性,交互动作进行抽象,封装以达到功能重用,隔离变化的目的。请看下面的例子:

Page model定义
class PageWithLogin
	def url
		#omitting code…
	end
	
	def login email, password
		#omitting code…
	end
end

class PageWithLoginResult
	def login_succeed?
		#omitting code…
	end
end
Step定义
Given /^the user login with the following detail:$/ do |detail|
	on_page_with :login do |page|
		visit page.url		
		page.login(detail["email"], detail["password"]) 
	end
end

Given /^the user should login succeed$/ do |detail|
	on_page_with :login_result do |page|
		page.login_succeed?.should == true
	end
end

如上,把loginlogin_succeed?功能封装到PageWithLogin, PageWithLoginResult这两个page model中,当”登录页面”,“登录成功页面”的页面结构发生变化时,只需要修改page model中的实现即可,step 定义无需任何变化。关于page model,我的同事徐昊曾经专门写过一篇文章

结论

BDD框架通过提供DSL,帮助业务人员,测试人员,开发人员定义需求的验收标准,共同得到一个明确的需求完成的定义。通过和webdriver集成,使这个验收标准变得可执行,大大减少了手工验证的压力,当软件通过了这个验收标准,则意味着这个需求已经开发完成。

注解与参考

  1. The truth about BDD Robert C Marting
  2. introducting BDD Dan North
  3. BDD on Wikipedia

感谢张凯峰对本文的审校。

Character encoding

ASCII

  • encoding in 7-bit.
  • 32 -> 127 representing characters.
  • 0 -> 31 representing control characters.
  • 128 -> 255 was called OEM characters, many company has their own idea about how to use these charaters.

ANSI:

  • lower 127 characters is same with ASCII.
  • higher 127 characters were divided into different “code pages”

Unicode:

  • Code point: In Unicode, a letter maps to something called a code point which is still just a theoretical concept.
  • Encoding: Unicode Byte Order Mark: indicating encoding order is ‘high-endian’ or ‘low-endian’

UTF8: Every code point from 0-127 is stored in a single byte. Only code points 128 and above are stored using 2, 3, in fact, up to 6 bytes.

The Most Important thing:

  It does not make sense to have a string without knowing what encoding it uses. 

怎样写单元测试

最近和同事讨论过几次如何写单元测试之后,突然意识到,是要写点什么了。
什么是单元测试, 维基百科上是这么定义的: 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执行的情况, 验证被测试对象的行为

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