文章目录
  1. 1. 序幕
  2. 2. 目录
  3. 3. 开发 Rails 应用程序
    1. 3.1. 配置
    2. 3.2. 路由
    3. 3.3. 控制器
    4. 3.4. 模型
      1. 3.4.1. ActiveRecord
      2. 3.4.2. ActiveResource
    5. 3.5. 迁移
    6. 3.6. 视图
    7. 3.7. 国际化
    8. 3.8. Assets
    9. 3.9. Mailers
    10. 3.10. Bundler
    11. 3.11. 无价的 Gems
    12. 3.12. 缺陷的 Gems
    13. 3.13. 管理进程
  4. 4. 测试 Rails 应用
    1. 4.1. Cucumber
    2. 4.2. RSpec
  5. 5. 延伸阅读

序幕

这份指南目的于演示一整套 Rails 3 开发的风格惯例及最佳实践。这是一份与由现存社群所驱动的Ruby 编码风格指南互补的指南。

而本指南中测试 Rails 应用小节摆在开发 Rails 应用之后,因为我相信行为驱动开发
(BDD) 是最佳的软体开发之道。铭记在心吧。

Rails 是一个坚持己见的框架,而这也是一份坚持己见的指南。在我的心里,我坚信 RSpec 优于 Test::Unit,Sass 优于 CSS 以及
Haml,(Slim) 优于 Erb。所以不要期望在这里找到 Test::Unit, CSS 及 Erb 的忠告。

某些忠告仅适用于 Rails 3.1+ 以上版本。

你可以使用 Transmuter 来产生本指南的一份 PDF 或 HTML 复本。

目录

本指南被翻译成下列语言:

开发 Rails 应用程序

配置

  • 把惯用的初始化代码放在 config/initializers。 在 initializers 内的代码于应用启动时执行。
  • 每一个 gem 相关的初始化代码应当使用同样的名称,放在不同的文件里,如: carrierwave.rb, active_admin.rb, 等等。
  • 相应调整配置开发、测试及生产环境(在 config/environments/ 下对应的文件)
    • 标记额外的资产给(如有任何)预编译:
1
2
3
# config/environments/production.rb
# 预编译额外的资产(application.js, application.css, 以及所有已经被加入的非 JS 或 CSS 的文件)
config.assets.precompile += %w( rails_admin/rails_admin.css rails_admin/rails_admin.js )
  • 将所有环境皆通用的配置档放在 config/application.rb 文件。
  • 构建一个与生产环境(production enviroment)相似的,一个额外的 staging 环境。

路由

  • 当你需要加入一个或多个动作至一个 RESTful 资源时(你真的需要吗?),使用 member and collection 路由。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 差
get 'subscriptions/:id/unsubscribe'
resources :subscriptions
# 好
resources :subscriptions do
get 'unsubscribe', on: :member
end
# 差
get 'photos/search'
resources :photos
# 好
resources :photos do
get 'search', on: :collection
end
  • 若你需要定义多个 member/collection 路由时,使用替代的区块语法(block syntax)。
1
2
3
4
5
6
7
8
9
10
11
12
13
resources :subscriptions do
member do
get 'unsubscribe'
# 更多路由
end
end
resources :photos do
collection do
get 'search'
# 更多路由
end
end
  • 使用嵌套路由(nested routes)来更佳地表达与 ActiveRecord 模型的关系。
1
2
3
4
5
6
7
8
9
10
11
12
class Post < ActiveRecord::Base
has_many :comments
end
class Comments < ActiveRecord::Base
belongs_to :post
end
# routes.rb
resources :posts do
resources :comments
end
  • 使用命名空间路由来群组相关的行为。
1
2
3
4
5
namespace :admin do
# Directs /admin/products/* to Admin::ProductsController
# (app/controllers/admin/products_controller.rb)
resources :products
end
  • 不要在控制器里使用留给后人般的疯狂路由(legacy wild controller route)。这种路由会让每个控制器的动作透过 GET 请求存取。
1
2
# 非常差
match ':controller(/:action(/:id(.:format)))'

控制器

  • 让你的控制器保持苗条 ― 它们应该只替视图层取出数据且不包含任何业务逻辑(所有业务逻辑应当放在模型里)。
  • 每个控制器的行动应当(理想上)只调用一个除了初始的 find 或 new 方法。
  • 控制器与视图之间共享不超过两个实例变量(instance variable)。

模型

  • 自由地引入不是 ActiveRecord 的类别吧。
  • 替模型命名有意义(但简短)且不带缩写的名字。
  • 如果你需要模型有著 ActiveRecord 行为的对象,比方说验证这一块,使用 ActiveAttr gem。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Message
include ActiveAttr::Model
attribute :name
attribute :email
attribute :content
attribute :priority
attr_accessible :name, :email, :content
validates_presence_of :name
validates_format_of :email, :with => /\A[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}\z/i
validates_length_of :content, :maximum => 500
end
更完整的示例,参考 [RailsCast on the subject](http://railscasts.com/episodes/326-activeattr)。

ActiveRecord

  • 避免改动缺省的 ActiveRecord(表的名字、主键,等等),除非你有一个非常好的理由(像是不受你控制的数据库)。
  • 把宏风格的方法(has_many, validates, 等等)放在类别定义的前面。
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
class User < ActiveRecord::Base
# 默认的scope放在最前面(如果有)
default_scope { where(active: true) }
# 接下来是常量
GENDERS = %w(male female)
# 然后放一些attr相关的宏
attr_accessor :formatted_date_of_birth
attr_accessible :login, :first_name, :last_name, :email, :password
# 仅接着是关联的宏
belongs_to :country
has_many :authentications, dependent: :destroy
# 以及宏的验证
validates :email, presence: true
validates :username, presence: true
validates :username, uniqueness: { case_sensitive: false }
validates :username, format: { with: /\A[A-Za-z][A-Za-z0-9._-]{2,19}\z/ }
validates :password, format: { with: /\A\S{8,128}\z/, allow_nil: true}
# 接着是回调
before_save :cook
before_save :update_username_lower
# 其它的宏 (像devise的) 应该放在回调的后面
...
end
  • 偏好 has_many :through 胜于 has_and_belongs_to_many。 使用 has_many :through 允许在 join 模型有附加的属性及验证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 使用 has_and_belongs_to_many
class User < ActiveRecord::Base
has_and_belongs_to_many :groups
end
class Group < ActiveRecord::Base
has_and_belongs_to_many :users
end
# 偏好方式 - using has_many :through
class User < ActiveRecord::Base
has_many :memberships
has_many :groups, through: :memberships
end
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :group
end
class Group < ActiveRecord::Base
has_many :memberships
has_many :users, through: :memberships
end
  • 使用新的 “sexy” validation
  • 当一个惯用的验证使用超过一次或验证是某个正则表达映射时,创建一个惯用的 validator 文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 差
class Person
validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i }
end
# 好
class EmailValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
record.errors[attribute] << (options[:message] || 'is not a valid email') unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
end
end
class Person
validates :email, email: true
end
  • 所有惯用的验证器应放在一个共享的 gem 。
  • 自由地使用命名的作用域(scope)。
1
2
3
4
5
6
class User < ActiveRecord::Base
scope :active, -> { where(active: true) }
scope :inactive, -> { where(active: false) }
scope :with_orders, -> { joins(:orders).select('distinct(users.id)') }
end
  • 将命名的作用域包在 lambda 里来惰性地初始化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 差劲
class User < ActiveRecord::Base
scope :active, where(active: true)
scope :inactive, where(active: false)
scope :with_orders, joins(:orders).select('distinct(users.id)')
end
# 好
class User < ActiveRecord::Base
scope :active, -> { where(active: true) }
scope :inactive, -> { where(active: false) }
scope :with_orders, -> { joins(:orders).select('distinct(users.id)') }
end
  • 当一个由 lambda 及参数定义的作用域变得过于复杂时,更好的方式是建一个作为同样用途的类别方法,并返回一个 ActiveRecord::Relation 对象。你也可以这么定义出更精简的作用域。
1
2
3
4
5
class User < ActiveRecord::Base
def self.with_orders
joins(:orders).select('distinct(users.id)')
end
end
  • 注意 update_attribute 方法的行为。它不运行模型验证(不同于 update_attributes )并且可能把模型状态给搞砸。
  • 使用用户友好的网址。在网址显示具描述性的模型属性,而不只是 id
    有不止一种方法可以达成:
    • 覆写模型的 to_param 方法。这是 Rails 用来给对象建构网址的方法。缺省的实作会以字串形式返回该 id 的记录。它可被另一个具人类可读的属性覆写。
1
2
3
4
5
class Person
def to_param
"#{id} #{name}".parameterize
end
end
为了要转换成对网址友好 (URL-friendly)的数值,字串应当调用 `parameterize` 。 对象的 `id` 要放在开头,以便给 ActiveRecord 的 `find` 方法查找。
  • 使用此 friendly_id gem。它允许藉由某些具描述性的模型属性,而不是用 id 来创建人类可读的网址。
1
2
3
4
class Person
extend FriendlyId
friendly_id :name, use: :slugged
end
    查看 [gem 文档](https://github.com/norman/friendly_id)获得更多关于使用的信息。

ActiveResource

  • 当 HTTP 响应是一个与存在的格式不同的格式时(XML 和 JSON),需要某些额外的格式解析,创一个你惯用的格式,并在类别中使用它。惯用的格式应当实作下列方法:extension, mime_type,
    encode 以及 decode
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
module ActiveResource
module Formats
module Extend
module CSVFormat
extend self
def extension
'csv'
end
def mime_type
'text/csv'
end
def encode(hash, options = nil)
# 数据以新格式编码并返回
end
def decode(csv)
# 数据以新格式解码并返回
end
end
end
end
end
class User < ActiveResource::Base
self.format = ActiveResource::Formats::Extend::CSVFormat
...
end
  • 若 HTTP 请求应当不扩展发送时,覆写 ActiveResource::Baseelement_pathcollection_path 方法,并移除扩展的部份。
1
2
3
4
5
6
7
8
9
10
11
12
13
class User < ActiveResource::Base
...
def self.collection_path(prefix_options = {}, query_options = nil)
prefix_options, query_options = split_options(prefix_options) if query_options.nil?
"#{prefix(prefix_options)}#{collection_name}#{query_string(query_options)}"
end
def self.element_path(id, prefix_options = {}, query_options = nil)
prefix_options, query_options = split_options(prefix_options) if query_options.nil?
"#{prefix(prefix_options)}#{collection_name}/#{URI.parser.escape id.to_s}#{query_string(query_options)}"
end
end
如有任何改动网址的需求时,这些方法也可以被覆写。

迁移

  • schema.rb 保存在版本管控之下。
  • 使用 rake db:scheme:load 取代 rake db:migrate 来初始化空的数据库。
  • 使用 rake db:test:prepare 来更新测试数据库的 schema。
  • 避免在表里设置缺省数据。使用模型层来取代。
1
2
3
def amount
self[:amount] or 0
end
然而 `self[:attr_name]` 的使用被视为相当常见的,你也可以考虑使用更罗嗦的(争议地可读性更高的) `read_attribute` 来取代:
1
2
3
def amount
read_attribute(:amount) or 0
end
  • 当编写建设性的迁移时(加入表或栏位),使用 Rails 3.1 的新方式来迁移 - 使用 change 方法取代 updown 方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 过去的方式
class AddNameToPerson < ActiveRecord::Migration
def up
add_column :persons, :name, :string
end
def down
remove_column :person, :name
end
end
# 新的偏好方式
class AddNameToPerson < ActiveRecord::Migration
def change
add_column :persons, :name, :string
end
end

视图

  • 不要直接从视图调用模型层。
  • 不要在视图构造复杂的格式,把它们输出到视图 helper 的一个方法或是模型。
  • 使用 partial 模版与布局来减少重复的代码。
  • 加入 client side validation 至惯用的 validators。 要做的步骤有:
    • 声明一个由 ClientSideValidations::Middleware::Base 而来的自定 validator
1
2
3
4
5
6
7
8
9
10
11
12
module ClientSideValidations::Middleware
class Email < Base
def response
if request.params[:email] =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
self.status = 200
else
self.status = 404
end
super
end
end
end
  • 建立一个新文件
    public/javascripts/rails.validations.custom.js.coffee 并在你的 application.js.coffee 文件加入一个它的参照:
1
2
# app/assets/javascripts/application.js.coffee
#= require rails.validations.custom
  • 添加你的用户端 validator:
1
2
3
4
5
6
7
8
#public/javascripts/rails.validations.custom.js.coffee
clientSideValidations.validators.remote['email'] = (element, options) ->
if $.ajax({
url: '/validators/email.json',
data: { email: element.val() },
async: false
}).status == 404
return options.message || 'invalid e-mail format'

国际化

  • 视图、模型与控制器里不应使用语言相关设置与字串。这些文字应搬到在 config/locales 下的语言文件里。
  • 当 ActiveRecord 模型的标签需要被翻译时,使用activerecord 作用域:
1
2
3
4
5
6
7
en:
activerecord:
models:
user: Member
attributes:
user:
name: "Full name"
然后 `User.model_name.human` 会返回 "Member" ,而 `User.human_attribute_name("name")` 会返回 "Full name"。这些属性的翻译会被视图作为标签使用。
  • 把在视图使用的文字与 ActiveRecord 的属性翻译分开。 把给模型使用的语言文件放在名为 models 的文件夹,给视图使用的文字放在名为 views 的文件夹。
    • 当使用额外目录的语言文件组织完成时,为了要载入这些目录,要在 application.rb 文件里描述这些目录。
1
2
# config/application.rb
config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')]
  • 把共享的本土化选项,像是日期或货币格式,放在 locales 的根目录下。
  • 使用精简形式的 I18n 方法: I18n.t 来取代 I18n.translate 以及使用 I18n.l 取代 I18n.localize
  • 使用 “懒惰” 查询视图中使用的文字。假设我们有以下结构:
1
2
3
4
en:
users:
show:
title: "User details page"
`users.show.title` 的数值能这样被 `app/views/users/show.html.haml` 查询:
1
= t '.title'
  • 在控制器与模型使用点分隔的键,来取代指定 :scope 选项。点分隔的调用更容易阅读及追踪层级。
1
2
3
4
5
# 这样子调用
I18n.t 'activerecord.errors.messages.record_invalid'
# 而不是这样
I18n.t :record_invalid, :scope => [:activerecord, :errors, :messages]
  • 关于 Rails i18n 更详细的信息可以在这里找到 Rails Guides

Assets

利用这个 assets pipeline 来管理应用的结构。

  • 保留 app/assets 给自定的样式表,Javascripts 或图片。
  • 把自己开发,但不适合用在这个应用的函式库,放在 lib/assets/
  • 第三方代码如: jQuerybootstrap 应放置在 vendor/assets
  • 当可能的时候,使用 gem 化的 assets 版本。(如: jquery-rails)。

Mailers

  • 把 mails 命名为 SomethingMailer。 没有 Mailer 字根的话,不能立即显现哪个是一个 Mailer,以及哪个视图与它有关。
  • 提供 HTML 与纯文本视图模版。
  • 在你的开发环境启用信件失败发送错误。这些错误缺省是被停用的。
1
2
3
# config/environments/development.rb
config.action_mailer.raise_delivery_errors = true
  • 在开发模式使用 smtp.gmail.com 设置 SMTP 服务器(当然了,除非你自己有本地 SMTP 服务器)。
1
2
3
4
5
6
# config/environments/development.rb
config.action_mailer.smtp_settings = {
address: 'smtp.gmail.com',
# 更多设置
}
  • 提供缺省的配置给主机名。
1
2
3
4
5
6
7
8
9
# config/environments/development.rb
config.action_mailer.default_url_options = {host: "#{local_ip}:3000"}
# config/environments/production.rb
config.action_mailer.default_url_options = {host: 'your_site.com'}
# 在你的 mailer 类
default_url_options[:host] = 'your_site.com'
  • 如果你需要在你的网站使用一个 email 链结,总是使用 _url 方法,而不是 _path 方法。 _url 方法包含了主机名,而 _path 方法没有。
1
2
3
4
5
6
7
# 错误
You can always find more info about this course
= link_to 'here', url_for(course_path(@course))
# 正确
You can always find more info about this course
= link_to 'here', url_for(course_url(@course))
  • 正确地显示寄与收件人地址的格式。使用下列格式:
1
2
# 在你的 mailer 类别
default from: 'Your Name <info@your_site.com>'
  • 确定测试环境的 email 发送方法设置为 test
1
2
3
# config/environments/test.rb
config.action_mailer.delivery_method = :test
  • 开发与生产环境的发送方法应为 smtp
1
2
3
# config/environments/development.rb, config/environments/production.rb
config.action_mailer.delivery_method = :smtp
  • 当发送 HTML email 时,所有样式应为行内样式,由于某些用户有关于外部样式的问题。某种程度上这使得更难管理及造成代码重用。有两个相似的 gem 可以转换样式,以及将它们放在对应的 html 标签里: premailer-rails3roadie

  • 应避免页面产生响应时寄送 email。若多个 email 寄送时,造成了页面载入延迟,以及请求可能逾时。使用 delayed_job gem 的帮助来克服在背景处理寄送 email 的问题。

Bundler

  • 把只给开发环境或测试环境的 gem 适当地分组放在 Gemfile 文件中。
  • 在你的项目中只使用公认的 gem。 如果你考虑引入某些鲜为人所知的 gem ,你应该先仔细复查一下它的源代码。
  • 关于多个开发者使用不同操作系统的项目,操作系统相关的 gem 缺省会产生一个经常变动的 Gemfile.lock 。 在 Gemfile 文件里,所有与 OS X 相关的 gem 放在 darwin 群组,而所有 Linux 相关的 gem 放在 linux 群组:
1
2
3
4
5
6
7
8
9
# Gemfile
group :darwin do
gem 'rb-fsevent'
gem 'growl'
end
group :linux do
gem 'rb-inotify'
end
要在对的环境获得合适的 gem,添加以下代码至 `config/application.rb` 
1
2
platform = RUBY_PLATFORM.match(/(linux|darwin)/)[0].to_sym
Bundler.require(platform)
  • 不要把 Gemfile.lock 文件从版本控制里移除。这不是随机产生的文件 - 它确保你所有的组员执行 bundle install 时,获得相同版本的 gem 。

无价的 Gems

一个最重要的编程理念是 “不要重造轮子!” 。若你遇到一个特定问题,你应该要在你开始前,看一下是否有存在的解决方案。下面是一些在很多 Rails 项目中 “无价的” gem 列表(全部兼容 Rails 3.1):

  • active_admin - 有了 ActiveAdmin,创建 Rails 应用的管理界面就像儿戏。你会有一个很好的仪表盘,图形化 CRUD 界面以及更多东西。非常灵活且可客制化。
  • better_errors - Better Errors 用更好更有效的错误页面,取代了 Rails 标准的错误页面。不仅可用在 Rails,任何将 Rack 当作中间件的 app 都可使用。
  • bullet - Bullet 就是为了帮助提升应用的效能(通过减少查询)而打造的 gem。会在你开发应用时,替你注意你的查询,并在需要 eager loading (N+1 查询)时、或是你在不必要的情况使用 eager loading 时,或是在应该要使用 counter cache 时,都会提醒你。
  • cancan - CanCan 是一个权限管理的 gem,
    让你可以管制用户可存取的支援。所有的授权都定义在一个档案里(ability.rb),并提供许多方便的方法,让你检查及确保整个应用内权限是否是可得的。
  • capybara - Capybara 旨在简化整合测试 Rack 应用的过程,像是 Rails、Sinatra 或 Merb。Capybara 模拟了真实用户使用 web 应用的互动。 它与你测试在运行的驱动无关,并原生搭载 Rack::Test 及 Selenium 支持。透过外部 gem 支持 HtmlUnit、WebKit 及 env.js 。与 RSpec & Cucumber 一起使用时工作良好。
  • carrierwave - Rails 最后一个文件上传解决方案。支持上传档案(及很多其它的酷玩意儿的)的本地储存与云储存。图片后处理与 ImageMagick 整合得非常好。
  • client_side_validations -
    一个美妙的 gem,替你从现有的服务器端模型验证自动产生 Javascript 用户端验证。高度推荐!
  • compass-rails - 一个优秀的 gem,添加了某些 css 框架的支持。包括了 sass mixin 的蒐集,让你减少 css 文件的代码并帮你解决浏览器兼容问题。
  • cucumber-rails - Cucumber 是一个由 Ruby 所写,开发功能测试的顶级工具。 cucumber-rails 提供了 Cucumber 的 Rails 整合。
  • devise - Devise 是 Rails 应用的一个完整解决方案。多数情况偏好使用 devise 来开始你的客制验证方案。
  • fabrication - 一个很好的假数据产生器(编辑者的选择)。
  • factory_girl - 另一个 Fabrication 的选择。一个成熟的假数据产生器。 Fabrication 的精神领袖先驱。
  • ffaker - 实用的 gem 来产生仿造的数据(名字、地址,等等)。
  • feedzirra - 非常快速及灵活的 RSS 或 Atom 种子解析器。
  • friendly_id - 透过使用某些具描述性的模型属性,而不是使用 id,允许你创建人类可读的网址。
  • globalize3 - Globalize3 是 Globalize 的后继者,针对 ActiveRecord 3.x 设计。基于新的 I18n API 打造而成,并帮 ActiveRecord 模型添加了事务功能。
  • guard - 极佳的 gem 监控文件变化及任务的调用。搭载了很多实用的扩充。远优于 autotest 与 watchr
  • haml-rails - haml-rails 提供了 Haml 的 Rails 整合。
  • haml - Haml 是一个简洁的模型语言,被很多人认为(包括我)远优于 Erb。
  • kaminari - 很棒的分页解决方案。
  • machinist - 假数据不好玩,Machinist 才好玩。
  • rspec-rails - RSpec 是 Test::MiniTest 的取代者。我不高度推荐 RSpec。 rspec-rails 提供了 RSpec 的 Rails 整合。
  • simple_form - 一旦用过 simple_form(或 formatastic),你就不想听到关于 Rails 缺省的表单。它是一个创造表单很棒的DSL。
  • simplecov-rcov - 为了 SimpleCov 打造的 RCov formatter。若你想使用 SimpleCov 搭配 Hudson 持续整合服务器,很有用。
  • simplecov - 代码覆盖率工具。不像 RCov,完全兼容 Ruby 1.9。产生精美的报告。必须用!
  • slim - Slim 是一个简洁的模版语言,被视为是远远优于 HAML(Erb 就更不用说了)的语言。唯一会阻止我大规模地使用它的是,主流 IDE 及编辑器对它的支持不好。但它的效能是非凡的。
  • spork - 一个给测试框架(RSpec 或 现今 Cucumber)用的 DRb 服务器,每次运行前确保分支出一个乾净的测试状态。 简单的说,预载很多测试环境的结果是大幅降低你的测试启动时间,绝对必须用!
  • sunspot - 基于 SOLR 的全文检索引擎。

这不是完整的清单,以及其它的 gem 也可以在之后加进来。以上清单上的所有 gems 皆经测试,处于活跃开发阶段,有社群以及代码的质量很高。

缺陷的 Gems

这是一个有问题的或被别的 gem 取代的 gem 清单。你应该在你的项目里避免使用它们。

  • rmagick - 这个 gem 因大量消耗内存而声名狼藉。使用 minimagick 来取代。
  • autotest - 自动测试的老旧解决方案。远不如 guard 及 watchr
  • rcov - 代码覆盖率工具,不兼容 Ruby 1.9。使用 SimpleCov 来取代。
  • therubyracer - 极度不鼓励在生产模式使用这个 gem,它消耗大量的内存。我会推荐使用 node.js 来取代。

这仍是一个完善中的清单。请告诉我受人欢迎但有缺陷的 gems 。

管理进程

  • 若你的项目依赖各种外部的进程使用 foreman 来管理它们。

测试 Rails 应用

也许 BDD 方法是实作一个新功能最好的方法。你从开始写一些高阶的测试(通常使用 Cucumber),然后使用这些测试来驱使你实作功能。一开始你给功能的视图写测试,并使用这些测试来创建相关的视图。之后,你创建丢给视图数据的控制器测试来实现控制器。最后你实作模型的测试以及模型自身。

Cucumber

  • @wip (工作进行中)标签标记你未完成的场景。这些场景不纳入考虑,且不标记为测试失败。当完成一个未完成场景且功能测试通过时,为了把此场景加至测试套件里,应该移除 @wip 标签。
  • 配置你的缺省配置文件,排除掉标记为 @javascript 的场景。它们使用浏览器来测试,推荐停用它们来增加一般场景的执行速度。
  • 替标记著 @javascript 的场景配置另一个配置文件。
    • 配置文件可在 cucumber.yml 文件里配置。
1
2
# 配置文件的定义:
profile_name: --tags @tag_name
  • 带指令运行一个配置文件:
1
cucumber -p profile_name
  • 若使用 fabrication 来替换假数据 (fixtures),使用预定义的 fabrication steps
  • 不要使用旧版的 web_steps.rb 步骤定义!最新版 Cucumber 已移除 web steps,使用它们导致冗赘的场景,而且它并没有正确地反映出应用的领域。
  • 当检查一元素的可视文字时,检查元素的文字而不是检查 id。这样可以查出 i18n 的问题。
  • 给同种类对象创建不同的功能特色:
1
2
3
4
5
6
7
8
9
10
11
12
13
# 差
Feature: Articles
# ... 功能实作 ...
# 好
Feature: Article Editing
# ... 功能实作 ...
Feature: Article Publishing
# ... 功能实作 ...
Feature: Article Search
# ... 功能实作 ...
  • 每一个功能有三个主要成分:
    • Title
    • Narrative - 简短说明这个特色关于什么。
    • Acceptance criteria - 每个由独立步骤组成的一套场景。
  • 最常见的格式称为 Connextra 格式。
1
2
3
In order to [benefit] ...
A [stakeholder]...
Wants to [feature] ...

这是最常见但不是要求的格式,叙述可以是依赖功能复杂度的任何文字。

  • 自由地使用场景概述使你的场景备作它用 (keep your scenarios DRY)。
1
2
3
4
5
6
7
8
Scenario Outline: User cannot register with invalid e-mail
When I try to register with an email "<email>"
Then I should see the error message "<error>"
Examples:
|email |error |
| |The e-mail is required|
|invalid email |is not a valid e-mail |
  • 场景的步骤放在 step_definitions 目录下的 .rb 文件。步骤文件命名惯例为 [description]_steps.rb。步骤根据不同的标准放在不同的文件里。每一个功能可能有一个步骤文件 (home_page_steps.rb)
    。也可能给每个特定对象的功能,建一个步骤文件 (articles_steps.rb)。
  • 使用多行步骤参数来避免重复
1
2
3
4
5
6
7
8
9
10
11
12
13
14
场景: User profile
Given I am logged in as a user "John Doe" with an e-mail "user@test.com"
When I go to my profile
Then I should see the following information:
|First name|John |
|Last name |Doe |
|E-mail |user@test.com|
# 步骤:
Then /^I should see the following information:$/ do |table|
table.raw.each do |field, value|
find_field(field).value.should =~ /#{value}/
end
end
  • 使用复合步骤使场景备作它用 (Keep your scenarios DRY)
1
2
3
4
5
6
7
8
9
10
11
12
13
# ...
When I subscribe for news from the category "Technical News"
# ...
# 步骤:
When /^I subscribe for news from the category "([^"]*)"$/ do |category|
steps %Q{
When I go to the news categories page
And I select the category #{category}
And I click the button "Subscribe for this category"
And I confirm the subscription
}
end
  • 总是使用 Capybara 否定匹配来取代正面情况搭配 should_not,它们会在给定的超时时重试匹配,允许你测试 ajax 动作。 见 Capybara 的 读我文件获得更多说明。

RSpec

  • 一个例子仅用一个期望值。
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
# 差
describe ArticlesController do
#...
describe 'GET new' do
it 'assigns new article and renders the new article template' do
get :new
assigns[:article].should be_a_new Article
response.should render_template :new
end
end
# ...
end
# 好
describe ArticlesController do
#...
describe 'GET new' do
it 'assigns a new article' do
get :new
assigns[:article].should be_a_new Article
end
it 'renders the new article template' do
get :new
response.should render_template :new
end
end
end
  • 大量使用 descibecontext
  • 如下地替 describe 区块命名:
    • 非方法使用 “description”
    • 实例方法使用井字号 “#method”
    • 类别方法使用点 “.method”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Article
def summary
#...
end
def self.latest
#...
end
end
# the spec...
describe Article do
describe '#summary' do
#...
end
describe '.latest' do
#...
end
end
  • 使用 fabricators 来创建测试对象。

  • 大量使用 mocks 与 stubs。

1
2
3
4
5
# mocking 一个模型
article = mock_model(Article)
# stubbing 一个方法
Article.stub(:find).with(article.id).and_return(article)
  • 当 mocking 一个模型时,使用 as_null_object 方法。它告诉输出仅监听我们预期的讯息,并忽略其它的讯息。
1
article = mock_model(Article).as_null_object
  • 使用 let 区块而不是 before(:each) 区块替 spec 例子创建数据。let 区块会被懒惰求值。
1
2
3
4
5
# 使用这个:
let(:article) { Fabricate(:article) }
# ... 而不是这个:
before(:each) { @article = Fabricate(:article) }
  • 当可能时,使用 subject
1
2
3
4
5
6
7
describe Article do
subject { Fabricate(:article) }
it 'is not published on creation' do
subject.should_not be_published
end
end
  • 如果可能的话,使用 specify。它是 it 的同义词,但在没 docstring 的情况下可读性更高。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 差
describe Article do
before { @article = Fabricate(:article) }
it 'is not published on creation' do
@article.should_not be_published
end
end
# 好
describe Article do
let(:article) { Fabricate(:article) }
specify { article.should_not be_published }
end
  • 当可能时,使用 its
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 差
describe Article do
subject { Fabricate(:article) }
it 'has the current date as creation date' do
subject.creation_date.should == Date.today
end
end
# 好
describe Article do
subject { Fabricate(:article) }
its(:creation_date) { should == Date.today }
end
  • Use shared_examples if you want to create a spec group that can be shared by many other tests.
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
# bad
describe Array do
subject { Array.new [7, 2, 4] }
context "initialized with 3 items" do
its(:size) { should eq(3) }
end
end
describe Set do
subject { Set.new [7, 2, 4] }
context "initialized with 3 items" do
its(:size) { should eq(3) }
end
end
#good
shared_examples "a collection" do
subject { described_class.new([7, 2, 4]) }
context "initialized with 3 items" do
its(:size) { should eq(3) }
end
end
describe Array do
it_behaves_like "a collection"
end
describe Set do
it_behaves_like "a collection"
end
### 视图
* 视图测试的目录结构要与 `app/views` 之中的相符。 举例来说,在 `app/views/users` 视图被放在 `spec/views/users`。
* 视图测试的命名惯例是添加 `_spec.rb` 至视图名字之后,举例来说,视图 `_form.html.haml` 有一个对应的测试叫做 `_form.html.haml_spec.rb`。
* 每个视图测试文件都需要 `spec_helper.rb`。
* 外部描述区块使用不含 `app/views` 部分的视图路径。 `render` 方法没有传入参数时,是这么使用的。

Ruby

# spec/views/articles/new.html.haml_spec.rb
require 'spec_helper'

describe 'articles/new.html.haml' do
  # ...
end
1
2
3
* 永远在视图测试来 mock 模型。视图的目的只是显示信息。
* `assign` 方法提供由控制器提供视图使用的实例变量(instance variable)。

Ruby

# spec/views/articles/edit.html.haml_spec.rb
describe 'articles/edit.html.haml' do
it 'renders the form for a new article creation' do
  assign(
    :article,
    mock_model(Article).as_new_record.as_null_object
  )
  render
  rendered.should have_selector('form',
    method: 'post',
    action: articles_path
  ) do |form|
    form.should have_selector('input', type: 'submit')
  end
end
1
2
* 偏好 capybara 否定情况选择器,胜于搭配正面情况的 should_not 。

Ruby

# 差
page.should_not have_selector('input', type: 'submit')
page.should_not have_xpath('tr')

# 好
page.should have_no_selector('input', type: 'submit')
page.should have_no_xpath('tr')
1
2
* 当一个视图使用 helper 方法时,这些方法需要被 stubbed。Stubbing 这些 helper 方法是在 `template` 完成的:

Ruby

# app/helpers/articles_helper.rb
class ArticlesHelper
  def formatted_date(date)
    # ...
  end
end

# app/views/articles/show.html.haml
= "Published at: #{formatted_date(@article.published_at)}"

# spec/views/articles/show.html.haml_spec.rb
describe 'articles/show.html.haml' do
  it 'displays the formatted date of article publishing' do
    article = mock_model(Article, published_at: Date.new(2012, 01, 01))
    assign(:article, article)

    template.stub(:formatted_date).with(article.published_at).and_return('01.01.2012')

    render
    rendered.should have_content('Published at: 01.01.2012')
  end
end
1
2
3
4
5
6
7
8
9
10
* 在 `spec/helpers` 目录的 helper specs 是与视图 specs 分开的。
### 控制器
* Mock 模型及 stub 他们的方法。测试控制器时不应依赖建模。
* 仅测试控制器需负责的行为:
* 执行特定的方法
* 从动作返回的数据 - assigns, 等等。
* 从动作返回的结果 - template render, redirect, 等等。

Ruby

    # 常用的控制器 spec 示例
    # spec/controllers/articles_controller_spec.rb
    # 我们只对控制器应执行的动作感兴趣
    # 所以我们 mock 模型及 stub 它的方法
    # 并且专注在控制器该做的事情上

    describe ArticlesController do
      # 模型将会在测试中被所有控制器的方法所使用
      let(:article) { mock_model(Article) }

      describe 'POST create' do
        before { Article.stub(:new).and_return(article) }

        it 'creates a new article with the given attributes' do
          Article.should_receive(:new).with(title: 'The New Article Title').and_return(article)
          post :create, message: { title: 'The New Article Title' }
        end

        it 'saves the article' do
          article.should_receive(:save)
          post :create
        end

        it 'redirects to the Articles index' do
          article.stub(:save)
          post :create
          response.should redirect_to(action: 'index')
        end
      end
    end
1
2
* 当控制器根据不同参数有不同行为时,使用 context。

Ruby

# 一个在控制器中使用 context 的典型例子是,对象正确保存时,使用创建,保存失败时更新。

describe ArticlesController do
  let(:article) { mock_model(Article) }

  describe 'POST create' do
    before { Article.stub(:new).and_return(article) }

    it 'creates a new article with the given attributes' do
      Article.should_receive(:new).with(title: 'The New Article Title').and_return(article)
      post :create, article: { title: 'The New Article Title' }
    end

    it 'saves the article' do
      article.should_receive(:save)
      post :create
    end

    context 'when the article saves successfully' do
      before { article.stub(:save).and_return(true) }

      it 'sets a flash[:notice] message' do
        post :create
        flash[:notice].should eq('The article was saved successfully.')
      end

      it 'redirects to the Articles index' do
        post :create
        response.should redirect_to(action: 'index')
      end
    end

    context 'when the article fails to save' do
      before { article.stub(:save).and_return(false) }

      it 'assigns @article' do
        post :create
        assigns[:article].should be_eql(article)
      end

      it 're-renders the "new" template' do
        post :create
        response.should render_template('new')
      end
    end
  end
end
1
2
3
4
5
6
7
### 模型
* 不要在自己的测试里 mock 模型。
* 使用捏造的东西来创建真的对象
* Mock 别的模型或子对象是可接受的。
* 在测试里建立所有例子的模型来避免重复。

Ruby
describe Article do
let(:article) { Fabricate(:article) }
end

1
2
* 加入一个例子确保捏造的模型是可行的。

Ruby
describe Article do
it ‘is valid with valid attributes’ do
article.should be_valid
end
end

1
2
* 当测试验证时,使用 `have(x).errors_on` 来指定要被验证的属性。使用 `be_valid` 不保证问题在目的的属性。

Ruby

# 差
describe '#title' do
  it 'is required' do
    article.title = nil
    article.should_not be_valid
  end
end

# 偏好
describe '#title' do
  it 'is required' do
    article.title = nil
    article.should have(1).error_on(:title)
  end
end
1
2
* 替每个有验证的属性加另一个 `describe`

Ruby
describe Article do
describe ‘#title’ do
it ‘is required’ do
article.title = nil
article.should have(1).error_on(:title)
end
end
end

1
2
* 当测试模型属性的独立性时,把其它对象命名为 `another_object`

Ruby
describe Article do
describe ‘#title’ do
it ‘is unique’ do
another_article = Fabricate.build(:article, title: article.title)
article.should have(1).error_on(:title)
end
end
end

1
2
3
4
5
6
7
8
9
### Mailers
* 在 Mailer 测试的模型应该要被 mock。 Mailer 不应依赖建模。
* Mailer 的测试应该确认如下:
* 这个 subject 是正确的
* 这个 receiver e-mail 是正确的
* 这个 e-mail 寄送至对的邮件地址
* 这个 e-mail 包含了需要的信息

Ruby
describe SubscriberMailer
let(:subscriber) { mock_model(Subscription, email: ‘johndoe@test.com’, name: ‘John Doe’) }

   describe 'successful registration email' do
     subject { SubscriptionMailer.successful_registration_email(subscriber) }

     its(:subject) { should == 'Successful Registration!' }
     its(:from) { should == ['info@your_site.com'] }
     its(:to) { should == [subscriber.email] }

     it 'contains the subscriber name' do
       subject.body.encoded.should match(subscriber.name)
     end
   end
 end
1
2
3
4
### Uploaders
* 我们如何测试上传器是否正确地调整大小。这里是一个 [carrierwave](https://github.com/jnicklas/carrierwave) 图片上传器的示例 spec:

Ruby

# rspec/uploaders/person_avatar_uploader_spec.rb
require 'spec_helper'
require 'carrierwave/test/matchers'

describe PersonAvatarUploader do
  include CarrierWave::Test::Matchers

  # 在执行例子前启用图片处理
  before(:all) do
    UserAvatarUploader.enable_processing = true
  end

  # 创建一个新的 uploader。模型被模仿为不依赖建模时的上传及调整图片。
  before(:each) do
    @uploader = PersonAvatarUploader.new(mock_model(Person).as_null_object)
    @uploader.store!(File.open(path_to_file))
  end

  # 执行完例子时停用图片处理
  after(:all) do
    UserAvatarUploader.enable_processing = false
  end

  # 测试图片是否不比给定的维度长
  context 'the default version' do
    it 'scales down an image to be no larger than 256 by 256 pixels' do
      @uploader.should be_no_larger_than(256, 256)
    end
  end

  # 测试图片是否有确切的维度
  context 'the thumb version' do
    it 'scales down an image to be exactly 64 by 64 pixels' do
      @uploader.thumb.should have_dimensions(64, 64)
    end
  end
end
```

延伸阅读

有几个绝妙讲述 Rails 风格的资源,若有闲暇时应当考虑延伸阅读:

文章目录
  1. 1. 序幕
  2. 2. 目录
  3. 3. 开发 Rails 应用程序
    1. 3.1. 配置
    2. 3.2. 路由
    3. 3.3. 控制器
    4. 3.4. 模型
      1. 3.4.1. ActiveRecord
      2. 3.4.2. ActiveResource
    5. 3.5. 迁移
    6. 3.6. 视图
    7. 3.7. 国际化
    8. 3.8. Assets
    9. 3.9. Mailers
    10. 3.10. Bundler
    11. 3.11. 无价的 Gems
    12. 3.12. 缺陷的 Gems
    13. 3.13. 管理进程
  4. 4. 测试 Rails 应用
    1. 4.1. Cucumber
    2. 4.2. RSpec
  5. 5. 延伸阅读