使用 Rspec, Capybara 和 Zeus 等测试 Rails 应用

1. Rails 应用测试策略

根据我个人的经验, 对于 Rails 应用需要做两方面的测试:

  • 单元测试: 针对 models, controllers, helpers, mailers 和 lib 下的代码.
    其中对 controllers 的测试不是从集成测试的角度, 而是从 是否可以访问到某些 actionaction 返回的 JSON 等数据是否正确 两个角度进行测试.
    单元测试属于 白盒测试.
    单元测试的代码位于 spec 文件夹下的同名的子文件夹, 例如 spec/models.

  • 验收测试: 也叫 集成测试, 是指模拟用户在浏览器的操作, 对网站的功能进行测试.
    验收测试以功能模块为单位进行测试, 例如用户注册, 添加文章等.
    验收测试属于 黑盒测试.
    验收测试的代码位于 spec/requests 文件夹下. (注: Cucumber 更适合用来做验收测试, 请参见续篇: 在 Rails 应用中使用 Cucumber 进行验收测试)

2. 测试 rails 应用用到的工具

针对版本: rails 3.2, ruby 1.9.3

  • rspec: BDD 测试框架, 替代 Rails 默认的 TestUnit.
  • factory_girl: 用于方便的创建测试数据, Rails Test Fixture 的替代品.
  • shoulda-matchers: 提供一些方便的测试 rails 应用的验证语句.
  • capybara: 验收测试框架, 用于模拟用户操作测试网站功能.
  • capybara-webkit: 为 capybara 提供 headless driver, 即在测试时不需要浏览器, 以便在 Linux 持续集成服务器上执行测试.
  • launchy: 在使用 capybara 测试时通过 save_and_open_save 方法来在浏览器打开当时状态的页面.
  • Database Cleaner: 用于在测试时清理数据库, 因为 capybara 不支持 rspec 默认的 transactional fixtures.
  • simplecov: 用于生成测试覆盖率报告.
  • zeus: 测试辅助工具, 用于加快执行测试和其他 Rails 命名的启动速度.

3. 配置及使用

注: 本文中使用的演示 rails 应用 (my-app) 的源代码存放在 github上, 以便随时参考或下载.

3.1 创建 rails 应用

shell
1
2
rails new my-app --skip-test-unit --skip-bundle
cd my-app

3.2 配置 Gemfile

添加以下内容到 Gemfile

Gemfile 完整文件
1
2
3
4
5
6
7
8
9
10
11
12
13
group :test, :development do
  gem 'rspec-rails', '~> 2.0'
end

group :test do
  gem 'factory_girl_rails'
  gem 'shoulda-matchers'
  gem 'capybara'
  gem 'capybara-webkit'
  gem 'launchy'
  gem 'database_cleaner'
  gem 'simplecov', require: false
end

如果你觉得官方的 rubygems 比较慢, 可以使用淘宝的 rubygems 源, 即把 Gemfile 第一行改为:

Gemfile 完整文件
1
source 'http://ruby.taobao.org/'

运行命令安装 gems:

shell
1
bundle install

3.3 配置 rspec

运行命令初始化 rspec:

shell
1
rails generate rspec:install

这个命令会创建两个文件: .spec 保存运行 rspec 时的需要使用的选项; spec/spec_helper.rb 是 rspec 的加载和配置文件, 所有的测试文件都需要 require 这个文件.

3.4 一个简单的测试

下面我们来写一个简单的单元测试. 假设我们需要实现一个方法用于产生指定长度的随机字符串, 我们采用 TDD 的方式, 即先写测试, 通过测试来 分析实现的方法保证代码的正确性.

单元测试文件的命名规则 是: 位于 spec 父文件夹下, 子文件夹的组织与要测试的文件一致, 测试文件的名字是 xxx_spec.rb (假设要测试的文件名是 xxx.rb).

我们给这个方法命名为 random_string, 并定义在 MyApp module 的 Utility 类中, 也就是说, 用 MyApp::Utility.random_string 来调用这个方法. 由于这个类属于我们自己的类库, 所以会放在 lib 文件夹下, 于是测试文件就应该是 spec/lib/my_app/utility_spec.rb. 我们先创建这个测试文件及它需要的文件夹结构 (本文中的命令针对于 Linux / Mac OS X 系统):

shell
1
2
mkdir -p spec/lib/my_app
touch spec/lib/my_app/utility_spec.rb

然后用你喜欢的编辑器打开这个测试文件, 输入:

spec/lib/my_app/utility_spec.rb 完整文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# encoding: utf-8

require 'spec_helper'

describe MyApp::Utility, '#random_string' do
  context '参数正确' do
    it '应该生成长度正确的字符串' do
      MyApp::Utility.random_string(0).should == ''
      MyApp::Utility.random_string(1).length.should == 1
      MyApp::Utility.random_string(10).length.should == 10
      MyApp::Utility.random_string(100).length.should == 100
    end

    it '多次生成的随机字符串应该不同' do
      MyApp::Utility.random_string(10).should_not == MyApp::Utility.random_string(10)
      # 或者这样测, 如果每次生成同样的字符串, results 数组 uniq 之后就会只有一个元素
      results = 3.times.map { MyApp::Utility.random_string(4) }
      results.uniq.length.should_not == 1
    end

    it '生成的字符串只包含大小写字母和数字' do
      10.times do
        MyApp::Utility.random_string(10).should =~ /\A[A-Za-z0-9]*\Z/
      end
    end
  end

  context '参数非法 - 指定长度为负数' do
    it '应该报错' do
      expect{ MyApp::Utility.random_string(-1) }.to raise_error
      expect{ MyApp::Utility.random_string(-10) }.to raise_error
    end
  end
end

由于 Rails 不会自动加载 lib 下的文件, 所以我们需要打开 config/application.rb 并找到 config.autoload_paths 这一行, 改为:

config/application.rb 完整文件
1
config.autoload_paths += %W(#{config.root}/lib)

然后执行测试. 虽然我们知道测试一定会失败, 因为我们根本还没有编写实现这个方法的文件, 但是 看着测试失败 也是 TDD 步骤的一部分, 是为了和写好实现方法之后测试通过的情况区分开.

shell
1
rspec spec/lib/my_app/utility_spec.rb

执行结果提示我们 MyApp::Utility 没有定义. 现在我们来写实现. 同样先创建需要的文件和文件夹结构:

shell
1
2
mkdir lib/my_app
touch lib/my_app/utility.rb

然后用编辑器打开文件并输入:

lib/my_app/utility.rb 完整文件
1
2
3
4
5
6
7
8
9
10
module MyApp::Utility
  def self.random_string(_length)
    length = _length.to_i
    raise "Wrong argument 'length' given: #{_length.inspect}" if length < 0

    chars = ('A'..'Z').to_a + ('a'..'z').to_a + ('0'..'9').to_a
    chars_length = chars.length
    length.times.map{ chars[rand(chars_length)] }.join
  end
end

再次执行测试, 并且反复进行 修改代码 -> 执行测试 直到测试通过.

rspec 测试判定方法及测试 Rails 应用的写法请参考 rspec 官方文档.

3.5 执行测试的方式

在 Rails 应用执行 rspec 测试有两种方式:

  • 使用 rake 命令. 示例:
shell
1
2
3
4
rake test # 执行所有测试, 也可以用 rake spec, 或直接用 rake 
rake spec:models # 执行 spec/models 下面的测试
rake spec:helpers # 执行 spec/helpers 下面的测试
rake spec:requests # 执行 spec/requests 下面的测试 (验收测试)

第一次执行这个命令时, 有可能会提示要先执行 rake db:migrate.

  • 使用 rspec 命令. 示例:
shell
1
2
3
4
5
6
7
8
9
10
11
12
13
rspec spec # 执行 spec 下面的测试, 即执行所有测试
rspec spec/models # 执行 spec/models 下面的测试
rspec spec/helpers # 执行 spec/helpers 下面的测试
rspec spec/requests # 执行 spec/requests 下面的测试 (验收测试)
rspec spec/lib # 执行 spec/lib 下面的测试

rspec spec/lib/my_app/utility_spec.rb # 只测试一个文件
rspec spec/lib/my_app/utility_spec.rb:7 # 只执行这个文件第7行所在的单个测试 (每个 "it" 块是一个测试)

rspec spec -t type:request # 执行 type 为 request 的测试, 即验收测试
rspec spec -t '~type:request' # 执行 type 不是 request 的测试

rspec spec -f d # 指定测试结果的格式为 documentation (文档格式)

详细的 rspec 使用方法可以执行 rspec --help 查看.

例如前面那个简单的测试, 使用 -f d 选项会将测试中的 context 等信息按非常便于阅读的格式打印出来:

rspec documentation 格式输出

如果想使用这个格式但不想每次执行测试时都手动指定 -f d 选项, 可以添加到 .rspec 文件里, 例如:

.rspec 完整文件
1
--color -f d

3.6 使用 factory_girl 构造测试数据

Rails 默认的 test fixture 可用来构造测试需要的数据, 但是不够灵活. factory_girl 提供了一个更灵活的解决方案.

为了演示, 假设我们需要有个 model Post (文章), 并且有个方法可以发布批量设置多篇文章, 即将给定数组里的所有文章的 published 属性设置为 true. 首先, 我们创建这个 model:

shell
1
rails g model Post title:string body:text published:boolean

然后执行数据库迁移命令和测试准备命令:

shell
1
2
rake db:migrate
rake db:test:prepare # 在数据库结构发生变化时, 需要执行这句来保证用 rspec 执行测试时不会出错

创建 model 时会自动生成对应的测试文件, 即 spec/models/post_spec.rb. 打开这个文件并编写测试代码:

spec/models/post_spec.rb 完整文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# encoding: utf-8

require 'spec_helper'

describe Post do
  it { should validate_presence_of(:title) }
  it { should validate_presence_of(:body) }
end

describe Post, '#publish_posts' do
  before do
    @posts = FactoryGirl.create_list :post, 10, published: false
  end

  context '全部发布' do
    before { Post.publish_posts(@posts) }
    it 'should work' do
      @posts.all?{ |post| post.published }.should be_true
      # 重新从数据库查询数据
      Post.all.all?{ |post| post.published }.should be_true
    end
  end

  context '部分发布' do
    before { Post.publish_posts(@posts.first(3)) }
    it 'should work' do
      @posts.first(3).all?{ |post| post.published }.should be_true
      Post.find(@posts.first(3).map(&:id)).all?{ |post| post.published }.should be_true

      @posts.last(7).all?{ |post| !post.published }.should be_true
      Post.find(@posts.last(7).map(&:id)).all?{ |post| !post.published }.should be_true
    end
  end
end

第5到8行是使用 shoulda 提供的测试判定语句来测试 model 的验证定义.

测试文件里使用了 FactoryGirl.create_list 方法来方便的创建多个 post. 为此, 我们需要定义 postfactory, 创建 spec/factories 文件夹并在下面新建 posts.rb 文件:

spec/factories/posts.rb 完整文件
1
2
3
4
5
6
7
FactoryGirl.define do
  factory :post do
    sequence(:title) { |n| "Post #{n}" }
    sequence(:body) { |n| "This is post #{n}" }
    published false
  end
end

这个文件定义了使用 FactoryGirl.create, FactoryGirl.create_list, FactoryGirl.build, FactoryGirl.build_list 等方法创建 post 时, 采用的默认属性值 (关于 factory_girl 的详细使用方法请参考其官方文档).

然后执行测试并确认测试是失败的:

shell
1
rspec spec/models/post_spec.rb

编写实现, 之后再次运行测试. 实现代码示例:

app/models/post.rb 完整文件
1
2
3
4
5
6
7
8
9
10
11
12
13
class Post < ActiveRecord::Base
  attr_accessible :title, :body, :published

  validates :title, :body, presence: true

  def self.publish_posts(posts)
    Post.transaction do
      posts.each do |post|
        post.update_attributes published: true
      end
    end
  end
end

3.7 使用 capybara 编写验收测试

(注: Cucumber 更适合用来做验收测试, 请参见续篇: 在 Rails 应用中使用 Cucumber 进行验收测试)

关于 capybara 的使用参考自 RailsCasts 257: Request Specs and Capybara. 下面列出配置步骤和一个测试示例.

首先配置 capybara. 打开 spec/spec_helper.rb 文件, 按下面的注释修改:

spec/spec_helper.rb 完整文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 在 require 'rspec/autorun' 这行下面添加
require 'capybara/rails'
require 'capybara/rspec'
Capybara.javascript_driver = :webkit

  # 下面这行配置由 true 改为 false
  config.use_transactional_fixtures = false

  # 在文件的最后一个 end 之前, 即 config.order = "random" 下面, 添加下面的代码
  config.before(:suite) do
    DatabaseCleaner.strategy = :truncation
  end

  config.before(:each) do
    DatabaseCleaner.start
  end

  config.after(:each) do
    DatabaseCleaner.clean
  end

下面是一个测试示例, 用来定义及验证文章列表和文章详情页面是否符合要求:

spec/requests/view_post_list_and_single_post_spec.rb 完整文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# encoding: utf-8

require 'spec_helper'

feature '查看文章列表和文章详情' do
  background do
    # 三个已发布的文章: Post 1, Post 2, Post 3
    @published_posts = FactoryGirl.create_list :post, 3, published: true
    # 两个未发布的文章: Post 4, Post 5
    @unpublished_posts = FactoryGirl.create_list :post, 2, published: false

    visit posts_path
  end

  scenario '可以看到已发布的文章及链接' do
    @published_posts.each do |post|
      page.should have_content(post.title)
    end
  end

  scenario '应该看不到未发布的文章' do
    @unpublished_posts.each do |post|
      page.should_not have_content(post.title)
    end
  end

  context '查看文章详情' do
    background do
      @post = @published_posts.first
      click_link @post.title
    end

    scenario '可以看到文章的标题和内容' do
      page.should have_content(@post.title)
      page.should have_content(@post.body)
    end

    scenario '可以通过 "返回" 链接回到文章列表' do
      click_link '返回'
      current_path.should == posts_path
    end
  end
end

具体实现请参考 项目源码.

3.8 使用 simplecov 查看测试覆盖率

首先需要配置 simplecov, 在 spec/spec_helper.rb 的第一行之前添加下面的代码:

spec/spec_helper.rb 完整文件
1
2
require 'simplecov'
SimpleCov.start 'rails'

以后在你运行过测试之后, 就可以用浏览器可以打开 coverage/index.html 查看测试覆盖率情况. 例如:

shell
1
2
3
4
5
6
7
rake spec # 运行所有测试
open coverage/index.html # 打开测试覆盖率报告, 此命令适用于 Mac OS X 系统

# Linux 用户可以直接调用浏览器的命令来打开, 例如使用 Chromium 浏览器:
chromium coverage/index.html
# 或者使用 Firefox:
firefox coverage/index.html

如果你使用源码版本控制工具, 例如 git, 别忘了把 coverage 文件夹加入忽略列表:

.gitignore 完整文件
1
/coverage

3.9 使用 zeus 加速测试

每次执行测试时都需要启动 rails 环境, 浪费不少时间. zeus 的原理就是启动好一个 rails 环境, 供之后的测试直接使用. 另外 rails console, rake 等命令也可以使用 zeus console, zeus rake 来快速执行.

首先安装并配置 zeus (不需要把 zeus 加到 Gemfile):

shell
1
2
gem install zeus
zeus init

会创建一个 zeus.json 的配置文件, 可以修改它来配置 zeus, 例如不需要 Cucumber 的话可以去掉 Cucumber 相关的配置.

这样, 在以后的开发过程中, 只要在一个 console 里启动 zeus, 就可以通过快速执行测试等命令了.

启动 zeus: zeus start

使用 zeus 执行测试需要使用 rspec 命令 (rake spec 在我这里会报错). 只要在测试命令前面加 zeus 即可, 例如:

shell
1
2
zeus rspec spec # 执行所有测试
zeus rspec spec/models/post_spec.rb # 执行单个测试文件

目前使用 zeus 执行测试会有一个 产生空白测试覆盖率报告 的 bug, 所以如果需要查看测试覆盖率, 就需要直接运行 rsepc 命令 (不要使用 zeus) 来运行一次测试, 然后查看测试覆盖率即可.

与 zeus 类似的工具有很多, 例如 spork. 在我使用 spork 时, 发现在测试 Active Admin 实现的页面时有问题, 而 zeus 没有问题, 并且 zeus 配置更加简单, 所以改用了 zeus.


续篇: 在 Rails 应用中使用 Cucumber 进行验收测试

庆祝 Clojure 五周岁

(转自 此Google Group帖子)

Rich Hickey 今天在 Clojure 讨论区发出了庆祝 Clojure 发布五周年。用这个例子回顾下我自己使用 Clojure 的历史。

我自己接触 Clojure 应该在3-4年前,大约是 Clojure 0.7.x 版本的时候开始使用。作为一个使用了 Java 近十年的程序员,我对 Java 的并行性难题和啰嗦的表达一直有些意见,所以在积极地寻找新的表达方式——新的语言来试图解决这些问题。

我首先尝试了 Groovy,这个语言基本可以用到 Java 的全部库,语法和 Java 相近,当然 Duck Typing 看起来很适合动态开发,加上各种爽利的语法糖,特别是关于 Collection 的语法糖,让这个选择看起来是学习曲线平缓、重用投资的理想选择。我使用它开发了几个分析工具,但在一次艰苦的排错中,我发现错误竟然来自编译器本身,一个理应合理的表达却产生不能预测的后果。造成我需要等待两次 Groovy 升级才能解决这个问题。这个问题提醒我,在考察一种新的语言的时候,它的编译器的成熟度和可靠性是多么重要。毕竟我们不想把自己的大厦建筑在沙地上。

其后一年我注意到了新的语言 Scala,它看起来也非常性感:动态类型推断、函数性编程(尽管那个时候我还不甚知道这个概念)、原生集合支持、各种强大的表达方式。不过,它在语法上的复杂性和多种语法糖,让我对它的可靠性颇有怀疑。尽管从2003年问世以来,彼时它已有五六年的历史。阅读它的邮件列表部分证实了我的推测:当时的发布版本中存在的编译器错误被承诺在下次解决。浏览它的版本历史中,可以清楚地看到影响表达的许多语法的 Bug 的记录。

然后我在 Tapestry 作者 Howard Lewiship 的博客上读到了 Clojure。

和许多人一样,我以前听说过传奇的 Lisp,但从来没想过试用它,毕竟它那怪异的表达和无穷的括号,看起来和我们需要不断解决问题的程序员的生活过于遥远。不过 Howard 的“下个世纪的编程语言”的论断让我决心一试。

它距离首次 alpha 发布才一年多!它目前才是 0.7-alpha 版本!这似乎与我想要的成熟度和可靠性相距有一光年。不过,简单地试用过以后,我被它迷住了:无比简洁和统一的 s-expression (谢谢你,来自 LISP 的伟大遗产),极为强大的集合处理能力,数据不可变性的宣言、强大到变态的宏。更令我自己惊讶的是,它的简单到了极点的本身的框架,和 core 的仅仅几千行代码。我感觉自己不需要再寻找外部的证据,因为这些简单的东西本身就很难产生 Bug。这全部要归功于其特别的抽象,这是真正的抽象的威力。对于我们来自 OO 世界的人,看到对象之外的表达可以如此强力,不由得目瞪口呆。后来和 Clojure Contrib 库的主要贡献者 Stuart Sierra 交流,他也说,即使他每天都在使用和创造 Clojure,也每天为这种威力所惊讶。

Stuart Halloway 的 Programming Clojure 是 Clojure 的第一本书,感谢这本书通过 Pragmatic Programmer 网上出版,迅速带领我领会到了 Clojure 的简单之美。用 Stu 的原话:使用 Clojure 让你经常会去想,这后面的概念到底是什么?大多数软件复杂性来源于不合适的抽象和不合适的概念。

我有幸在 Clojure 1.1 发布后,到美国与 Stuart Halloway, Stuart Sierra, Aaron Bedra 这样的 Clojure 创造团队一同工作了一个月,对于这种语言的应用范围、它的威力、它的局限都有了更深的了解。对于我来说,Clojure 是一种很难过时的工具:它的表达和基础实在是太简单和太灵活了。尽管它还很年轻,它太过要求程序员的理解能力、它的陡峭的学习曲线、它相对缺乏开发工具,但这绝对不妨碍它成为你的编程工具箱里的终极武器。

我也参加了第一次 Clojure Conj 大会,看到很多活跃的 Clojure 程序员兴奋地来到这首次聚会。Mark Mcgranaghan (mmcgrana, Web 框架 Ring 的作者) 的演讲 Lord of Rings, Christophe Grand (cgrand,enlive 的作者)的谁也听不懂的演讲(法国人说英语的悲剧),和纯黑客打扮出场的 Phil Hagelberg (technomacy,Leiningen 的作者,他穿着带键盘的裤子)都是会上的热点。不过,Rich Hickey 的演讲《吊床驱动的编程》才是热点中的热点。

Rich Hickey 不仅仅是 Clojure 之父,他确实有着极深的思维深度。不仅对于程序语言,对于数据库(在我们的项目中共享数据的分布式框架,由 Rich Hickey 出谋划策,对于 Datomic 数据库的出现说不定有间接作用)等方面都有非常独到的认识。顺便说一句,他是美国少见的围棋迷,围棋的一黑一白构成的简洁但变化无穷的规则相信对他有着强大的影响,在他主导的框架中,简单总是在所有价值中占据最重要的一部分。

在这个新的语言层出不穷的时代,大多数语言仅仅在理论上存在过,大多数在小的应用领域存在。很多语言依靠跨国巨头的强大推力,但很少有 Clojure 这样产生于黑客社区,并纯粹靠黑客的爱好来发展和推动的语言可以获得如此多的关注。这是因为其他语言可能只是工具,但 Clojure 才是少见的真正揭示编程之美、令你无条件入迷的那种东西。

“重剑无锋,大巧不工”。

Web 开发流程 (使用git和redmine)

1. 分析任务及估算工作量

1.1 如果是新功能, 则阅读并理解此功能相关的用例图、活动图、原型图.

1.2 如果是问题, 则重现BUG: 对于可以通过自动化测试判断的, 修改测试代码创造bug出现的条件, 然后运行测试确保测试失败; 否则通过手工方式重现.

1.3 如果需要修改代码, 则使用git创建一个新分支并push到origin.

示例:

1
2
3
git fetch # 获取 origin/master 的最新状态
git checkout -b feature123-user-sign-up origin/master # 以 origin/master 为基准创建新分支, 并切换到新创建的分支
git push origin feature123-user-sign-up # 将新分支 push 到 origin

分支命名规范: 类型及编号-简要-描述, 例如:

1
2
3
4
5
6
# 功能
feature123-user-sign-up
# 问题
bug456-user-can-not-sign-in
# 任务
task789-collect-user-feedbacks

1.4 最后, 分析实现方法并估算工作量, 并更新 issue:

  状态 -> 处理中
  开始日期 -> 若之前未设置则设为当天
  完成日期 和 预期时间 -> 根据自己的估算进行更新
  %%完成 -> 10%%

2. 实现

2.1 如果需要修改代码

  • 采用 测试驱动开发 (TDD), 即先写测试再写实现, 并达到测试覆盖率(90%)等要求.
  • 确保在此任务关联的分支下进行开发. 开发过程中要经常提交, 合并远程同名分支和 master 分支, 并 push 到远程服务器.
    提交的频率一般是每半个小时一次, 合并+push的频率一般是每小时一次
示例:

1
2
3
4
5
6
7
git status # 查看状态, 确保是在自己创建的分支中进行的开发 (不要在master分支下!)
git commit # 注意配合 git add 等命令进行提交, 以及 "-a", "-m" 等选项
git status # 确认已经提交并处于 clean 状态
git fetch # 获取 origin/master 的最新状态
git merge origin/feature123-user-sign-up # 合并远程同名分支, 以得到其他人在此分支下的提交. 如果有冲突, 就解决冲突然后提交 (使用git add及git commit命令)
git merge origin/master # 合并远程master分支最新的提交到自己的分支, 如果有冲突, 就解决冲突然后提交 (使用git add 及git commit命令)
git push origin feature123-user-sign-up # push 到远程服务器

  • 开发完毕后, 确保已将此任务相关的所有代码修改做了上面的步骤 (即提交, 合并和push). 然后修改 issue:
  状态 -> 待评审
  指派给 -> 评审者
  %%完成 -> 100%%

2.2 如果不需要修改代码

  • 如果是整理文档等任务, 将整理完成的文档上传到issue页面.
  • 任务完成后, 更新issue:
  状态 -> 待确认
  指派给 -> 确认者
  %%完成 -> 100%%

3. 评审 (review)

(只针对需要评审的情况, 即状态为 待评审 时)

3.1 进行评审

  • 如果代码发生了变化, 评审者需要切换到此任务的分支并进行评审. 示例:
    1
    2
    3
    4
    5
    
    git fetch # 获取远程所有分支的最新状态
    git branch -r # 列出所有 origin 下的分支, 得到跟此任务相关的分支名 (即已任务的redmine类型和编号开头的), 假设是 feature123-user-sign-up
    git checkout feature123-user-sign-up # 切换到任务相关的分支
    git merge origin/feature123-user-sign-up # 如果之前切换到此分支过, 则需要执行这句来更新到最新状态
    git diff origin/master # 查看分支的提交变更, 进行review
    
  • 如果没有修改代码, 而是整理文档的任务, 则评审者对整理完成的文档进行评审.

3.2 如果评审成功 (没有发现问题)

  • 将代码 merge 到 master. 示例:
    1
    2
    3
    4
    5
    6
    
    git checkout master # 切换到 master 分支
    git pull origin master # 同步 master 为远程服务器上的最新状态
    git merge feature123-user-sign-up # 合并 review 过的分支
    #如果出现了冲突, 则视为 review 出现问题, 将issue反馈给原开发人员, 要求他重新合并+push, 完成后重新评审.
    git push origin :feature123-user-sign-up # 从远程删除此分支
    git branch -d feature123-user-sign-up # 删除本地分支
    
  • 部署. 示例:
    1
    2
    3
    4
    
    cap deploy:migrations # 部署至 alpha 环境
    #部署完毕后查看在 alpha 上的效果, 没有问题的话:
    cap production deploy:migrations # 部署至 production 环境
    #部署完毕后打开网站查看下有无问题
    
  • 更新issue. 状态 设为 待确认, 指派给 设为 确认者.
    对于不需要确认的情况, 则可以直接将issue的状态设为 “已关闭”.

3.3 如果评审失败 (存在问题)

  • 评审者更新issue:
  状态 -> 反馈
  指派给 -> 原开发人员
  说明 -> 列出存在的问题, 如果是截图则已附件的形式上传
  • 指派给原开发人员后, 原开发人员需要
  1. 确认发现的问题
  2. 更新issue: 状态 设置为 "处理中", %%完成 设置为非100%%的值.
  3. 处理发现的问题, 如修改代码或文档. 此过程和 *2. 实现* 一样, 包括处理完问题后的步骤.

4. 确认实现

4.1 确认者确认任务是否完成 (或者功能是否实现, 或者问题是否修复). 对于功能和问题, 需要访问 production 环境的网站进行确认.

4.2 如果确认成功 (没有发现问题), 则更新issue: 状态 设为 已关闭.

4.3 如果确认失败 (存在问题)

  • 确认者更新issue
  状态 -> 反馈
  指派给 -> 原开发人员
  说明 -> 描述发现的问题, 如果有截图则上传为附件
  • 指派给原开发人员后, 原开发人员需要
  1. 确认发现的问题
  2. 更新issue: 状态 设置为 "处理中", %完成 设置为非100%的值.
  3. 处理发现的问题, 如修改代码或文档. 此过程和 2. 实现 一样, 包括处理完问题后的步骤.

Web 开发 - Git 流程

参考 GitHub flow

  1. master分支上代码一定是test过,review过,可以直接上线的

  2. 修改代码时先创建一个分支,经常提交, 经常merge master到本地分支,并经常push到remote server上的同名分支

  3. 功能完成并通过测试后,需要经过另外至少一个人的review才能merge回master

  4. merge分支到master之后立即deploy

Rails ActiveRecord Callbacks

Rails 提供的约定非常强大而且灵活, 不过要想充分受益, 前提是很好的掌握各种约定的使用方法. 最近我就遇到了由于忽略 active-record callback 的一个约定而导致的奇怪 “问题”.

问题的现象是当保存一个 Model 的对象时, 结果被 rollback 并且抛出了 ActiveRecord::RecordInvalid 错误. 当我查看这个对象的 errors 时, 发现并没有错误, valid? 也返回 true.

最终我发现是由于我写的一个 before_create 的 callback 无意中返回了 false 也导致对象保存失败. 查看了 Rails 官方的 guides 描述如下:

The whole callback chain is wrapped in a transaction. If any before callback method returns exactly false or raises an exception, the execution chain gets halted and a ROLLBACK is issued; after callbacks can only accomplish that by raising an exception.

就是说, 如果某个 before-callback 返回 false 或者抛出异常, 就会立即中断其他还未执行的 callback 以及保存对象的操作. 而我的那个 before-callback 类似于:

1
2
3
4
5
6
before_create :init_attributes

private
def init_attributes
  self.processed = false if self.processed.nil?
end

这个 callback 的作用是在新建对象时, 如果没有给 processed 赋值就设置默认值为 false. 但在 Ruby 中, 一个方法的最后一个表达式就是方法的返回值, 所以这个 callback 无意中在设置默认值的同时返回了 false, 导致了对象在没有任何错误的情况下直接无法保存.

低级错误, 谨记.

最佳实践之 Javascript MVC

最近 web 开发的一个趋势是使用 Javascript MVC, 核心思想是在客户端(浏览器端)实现大部分的操作,比如数据的展示形式(HTML)及交互的控制等。

与传统 web 开发流程的区别

Javascript MVC 方法与使用 Ruby on Rails, Django, Struts 等框架的传统开发流程的主要区别,就是在客户端处理 HTML 视图。传统开发流程一般是在服务器端将需要显示给用户的 HTML 生成好,然后通过普通 HTTP 响应或者 AJAX 响应返回到用户的浏览器。而 Javascript MVC 直接在浏览器中根据保存的 HTML 模板和从服务器端得到的数据(一般是 JSON 格式)生成 HTML,然后就可以直接显示给用户。

这种流程最大的好处就是使得 web 开发层次更加清晰。传统的开发流程,服务器端代码既要处理业务逻辑代码,又要负责将数据装入视图模板,而后一个工作完全可以交给客户端代码来处理。因为本来客户端代码也是无法避免的,总会有比如控制 HTML 元素是否显示、更改样式等需求,而将数据展现出来跟这些需求没有本质的区别。

由客户端渲染 HTML 模板可能会让人担心存在效率问题。事实上这个完全不必担心,现存的很多优秀的 HTML 模板框架的表现很好(Mustache, Handlebars 等),而且各个浏览器的 Javascript 解释引擎也日益强大。

不仅仅是在客户端渲染 HTML

正如前面说的,会有一些常用的需求是需要在客户端实现的。Javascript MVC 通常会提供 UI Binding 功能,使得实现这些需求更加方便。所谓 UI Binding,就是当 HTML 元素中的数据发生变化时,不需要手动更新 HTML 元素的内容(比如通过使用 jQuery 选择器设置 text 或 html),HTML 元素的内容就会自动更新。

你可以看下 这个简单的例子 感受一下。在这个简单的 TODO web 应用里,所有操作都没有手动去设置 HTML 元素的内容,都是通过 UI Binding 自动更新的。

那服务器端做些什么呢

Javascript MVC 不仅做了本应由服务器端处理的 HTML 模板方面的工作(MVC 中的 View,或 Template),而且也有 Model 和 Controller 层,那么服务器端要做什么呢?其实 Javascript MVC 的主要目的是将 数据的展示数据本身的处理 分开,自己来做前一部分,而数据的处理比如查找、修改、删除、新建等就要跟数据库打交道了,这正是服务器代码应该处理的事情(现在好像 CouchDB 这种工具也可以在客户端操作数据库了)。所以,需要有一个方法供 Javascript MVC 和服务器端代码进行交互,使得客户端可以收集数据并交给服务器端处理、从服务器端得到需要显示的数据等。这个方法的最佳选择就是 REST,加上 AJAX 和 JSON。

有了 REST,Javascript 的 Model 层就可以将数据的操作通过 REST API 交由服务器端处理。如果通过 ember-rest 等小工具来自动绑定 Model 操作至 AJAX 请求,就更加方便了,例如下面的代码(CoffeeScript 代码,相当于 Javascript 代码):

1
2
3
4
5
6
7
8
9
10
App.NewPostView = Em.View.extend
  submit: (event) ->
    self = this
    post = @get('post')
    event.preventDefault()
    post.saveResource()
      .fail (e) ->
        App.displayError(e)
      .done ->
        App.postsC.pushObject post

其中的 post.saveResource() 会自动调用 AJAX 请求并将新 post 的数据发送给服务器端,由服务器端代码将新数据添加到数据库。

也就是说,有了 Javascript MVC,服务器端代码就可以专心处理那些服务器端代码应该处理的事,只需要提供标准的 REST API 来与客户端代码传递数据。例如如果使用 Ruby on Rails,那么 ERB/HAML 代码会大大减少, Model 和 Controller 代码可以保留或稍加调整;而且 Rails 对 REST 的默认支持大大简化了与 Javascript MVC 的交互。就一个字,爽!

有哪些好用的 Javascript MVC 框架呢

程序员是幸福的,因为总是有好用的框架或工具可用(不需要重新发明轮子,当然如果实在没有好轮子大可以自己造一个 ;)

最有名的 Javascript MVC 框架要属 Backbone.js 了。但是经过对比,我个人推荐 Ember.js,因为它提供了上面讲的功能,尤其是 UI Binding 和 Observer。关于其他一些 Javascripts MVC 框架的对比可以参考这里。(更新: 在尝试了 Ember.js 和 Angular.js 之后,我最终选择了 Angular.js)

对 Ember.js 感兴趣的同学可以参考 这篇英文教学,也可以查看我根据这篇教学写的 ember-rails-demo(主要的 Javascript 代码在 app/assets/javascripts/app 文件夹)。

最佳实践之 Less 基础

这个 less 是指 Unix/Linux/Mac OS 下的 less 命令行工具, 用来查看文本文件, 尤其是在查看大文本文件时也毫无压力.

less 常用快捷键

1
2
3
4
5
6
j, k - 下一行, 上一行
f, b - 下一屏, 上一屏 (forward, back)
d, u - 下半屏, 上半屏 (down, up)
/, ? - 向下搜索, 向上搜索
g, G, 10g - 到第一行, 到最后一行, 到第10行
Ctrl+g - 显示当前文本状态, 包括当前所在行, 总行数, 当前位置所占百分比

最佳实践之 Git 基础

git 是一个强大而快速的代码版本管理工具. 关于 git 的背景请参考 Git历险记(一).

准备 ssh public key

使用 git 向远程代码仓库提交代码时, 最常用的验证方法是通过 ssh. 这需要先将自己的 ssh public key 传到到代码仓库所在的服务器上, 可以通过服务器上安装的 git 托管软件来配置, 比如 Gitosis 或 Gitolite.

ssh public key 通常位于家目录的 .ssh 目录下, 比如: ~/.ssh/id_rsa.pub. 如果还没有 ssh public key, 可以通过 ssh-keygen 命令生成, 例如: ssh-keygen -t rsa. 生成过程中会询问是否设置 passphrase, 这个是当使用次 ssh key 时需要额外验证的密码, 如果对安全要求特别高可以设置, 否则可以直接按回车忽略.

基本配置

配置用户名和邮箱, 例如:

1
2
git config --global user.name "xhh"
git config --global user.email "xhh@xhh.me"

配置常用命令的别名以方便操作, 例如:

1
2
3
4
5
6
git config --global alias.s status
git config --global alias.co checkout
git config --global alias.df diff
git config --global alias.ci commit
git config --global alias.br branch
git config --global alias.amend "commit --amend"

其他配置, 例如显示的格式和颜色, 自动处理换行符等

1
2
3
git config --global color.ui auto
git config --global log.date local
git config --global core.autocrlf input

以上的配置都使用了 –global 选项, 这样配置就会写进全局配置文件 ~/.gitconfig, 供所有 git 项目使用. 也可以直接修改这个文件来添加或修改上述配置.
每个 git 项目都有一个 .git 文件夹, 此文件夹下的 config 文件存储了特定这个项目的 git 配置, 就是说这些配置会覆盖 ~/.gitconfig 中的配置.

具体配置参考 git help config

.gitconfig 示例

日常工作流程

克隆项目

1
2
git clone git@my-git-server:project.git
cd project

从服务器同步分支 (更新本地分支)

例如同步 master 分支:

1
2
git co master
git pull origin master

开发新功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
git co -b my-new-feature
# 在本地创建并切换到新分支, 如果已经创建过直接用 git co my-new-feature

# 编写代码
# ....
# 查看文件修改状态
git s
# 查看本地所做的更改
git df
# 如果已经add了更改, 使用
git df --cached

git add file-to-add
# 如果需要提交所有 app 文件夹下新增的文件, 可以使用 git add app
# 如果需要提交当前文件夹下所有新增的文件, 使用 git add .
# 如果 add 了不想提交的文件, 使用 git reset file-not-to-add
# 如果想把文件从版本库里删除, 可以直接删除并在提交是使用 git commit -a
# 或者使用 git rm file-to-remove
# 如果是删除文件夹, 使用 git rm -r folder-to-remove
# 下面是提交并编写提交消息
git commit # 此时会打开编辑器 (例如vim) 供填写提交消息, 即此次提交代码的描述.
# 如果想直接在提交的同时提供提交消息, 使用 git commit -m "my commit message"

# 如果没有新增的文件需要 add, 即刚才仅仅修改或删除了文件, 那么上面两步可以简写为 git commit -a
# 或 git commit -am "my commit message"

# 如果要把当前分支提交到远程仓库, 共享给别人或等待review, 使用:
git push origin my-new-feature
# 如果想在本地将此分支 merge 到 master 分支上, 使用:
git rebase master # 如果存在冲突, 在解决冲突之后
# 使用 git add conflicted-files
# 然后 git commit
# 然后 git rebase --continue
# ( 如果想终止 rebase 操作, 使用 git rebase --abort )
# rebase 操作完成后
git co master
git merge my-new-feature
# 然后可以将本地的提交推送到远程仓库: git push origin master
# 第一次推送到远程仓库时可以使用 git push -u origin master
# 这样以后就可以直接使用 git push 和 git pull 了

# 如果想删除本地分支, 使用
git br -d my-new-feature
# 如果想删除远程仓库的分支, push时在分支前面加上冒号
git push origin :my-new-feature

# 查考提交记录
git log
# 如果提交记录超过一屏, 会自动使用 less 查看, 常用快捷键参考下面的 "less 基础" 链接
# 只查看最近3次提交
git log -3
# 同时查看代码行数变化
git log --stat # 使用+-表示
git log --numstat # 使用数字表示
# 同时查看代码详细更改
git log -p
# 按时间顺序显示最近24小时的提交, 并查看行数变化和代码更改
git log -p --since=yesterday --reverse --stat
# 查看一个文件的每一行的最后修改人
git blame file-to-check

less 基础

Tar 错误: Ignoring Unknown Extended Header Keyword ‘SCHILY.dev’

在 Mac OS 使用 tar 创建的压缩包, 传到 Ubuntu 服务器上解压时提示

1
2
3
tar: Ignoring unknown extended header keyword `SCHILY.dev'
tar: Ignoring unknown extended header keyword `SCHILY.ino'
tar: Ignoring unknown extended header keyword `SCHILY.nlink

根据以下两个链接得知是因为 Mac OS 默认使用 bsdtar, 而 Ubuntu 则是 gnutar. http://ocdevel.com/blog/snow-leopard-tarball-uploading-issues http://help.directadmin.com/item.php?id=220

于是在 Mac OS 下安装 gnutar (自带的版本有点低) 并设置成默认:

1
2
3
brew install gnu-tar
sudo ln -fs /usr/local/bin/gtar /usr/bin/tar
tar --version

Clojure 新手指南

(注: 本文翻译自 The Blackstag Blog)
(注: 翻译水准参差不齐, 慎入)

Clojure 新手指南

很久之前我决定开始写博客. 基于某些原因当时我决定自己编写一个博客网站, 我认为这比直接使用现有的博客引擎更好. 我不打算详细解释当时这样选择的理由, 不过原因之一是可以借此学习一门新的语言. 这个决策也造就了现在的我 - 深入的使用 Clojure 编程并且写博客分享我喜欢的这门语言.

为什么选择 Clojure?

最开始, 我选择使用 Clojure 是因为她的以下优点: 速度快, 方便访问 Java 类库, 优秀的并行计算和并发编程能力, 支持函数编程范式, 以及强大的 “代码宏” (代码生成代码). 然而, 虽然这些特性的确很强大, 促使我至今一直使用 Clojure 的原因却不在此列.

经过了几个月的使用, 我发现对于 Clojure 我最喜欢的一点是, 她在提供了很多其他语言拥有的良好特性的同时, 没有以某种形式限制她的使用者, 在强大和灵活之间她做到了很好的平衡. 例如, 我发现我可以很方便的使用几乎所有现有的 Java 类库, 但不需要花费大量的时间跟 Java 打交道; 我可以使用函数式的编程思维来实现某个想法, 也可以使用面向对象的方式; 我可以在需要的时候明确的声明变量类型, 也可以在不需要的时候选择不声明, 让 Clojure 来确定变量的类型, 同时减少那些容易分散注意力的声明变量类型的代码. 上面的这些同时表明 Clojure 的强大与灵活的例子不胜枚举.

最终, Clojure 带给我真正重要的特性是对复杂代码的良好抽象. 当使用非常强大的特性实现了非常强大的功能, 对应的代码通常也变得非常复杂, 但 Clojure 却能hold住. 这让我可以将更多的时间用于思考如何实现业务逻辑, 而不是思考如何把想到的方法变成代码. 由于这一点, 使用 Clojure 让我实现了更多的想法并获得更多的成绩, 也让我在编程工作体会到了前所未有的快乐.

内容和范围

Clojure 是一门拥有丰富特性的语言, 以至于当一个人仅仅是写一个关于 Clojure 的博客时, 写着写着就发现自己是在写一本书 -.-! 所以你可能会发现这个指南没有包含很多 Clojure 提供的特性, 因为我只写了我认为对于一个新手来说有必要知道的内容.

Clojure 指南: 目录

Clojure 指南 - 第一章: 安装与配置 (TODO)

说明 (2012-07-17)

又一次,我取消了这个翻译计划,主要原因是自己比较懒,次要原因是发现了下面两个很好的Clojure中文学习资料:

Clojure入门教程

Clojure Handbook

另外,我开始看 Practical Clojure 的电子书,真心觉得不错。