序幕
这份指南目的于演示一整套 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.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
resources :posts do
resources :comments
end
1
2
3
4
5
namespace :admin do
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
default_scope { where(active: true ) }
GENDERS = %w(male female)
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
...
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
class User < ActiveRecord::Base
has_and_belongs_to_many :groups
end
class Group < ActiveRecord::Base
has_and_belongs_to_many :users
end
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
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)的数值,字串应当调用 `p arameterize` 。 对象的 `i d` 要放在开头,以便给 ActiveRecord 的 `f ind` 方法查找。
使用此 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::Base 的 element_path 及 collection_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]` 的使用被视为相当常见的,你也可以考虑使用更罗嗦的(争议地可读性更高的) `r ead_attribute` 来取代:
1
2
3
def amount
read_attribute(:amount ) or 0
end
当编写建设性的迁移时(加入表或栏位),使用 Rails 3.1 的新方式来迁移 - 使用 change 方法取代 up 与 down 方法。
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
3
4
5
6
7
8
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"
然后 `U ser.model_name.human` 会返回 "Member" ,而 `U ser.human_attribute_name("name" )` 会返回 "Full name" 。这些属性的翻译会被视图作为标签使用。
把在视图使用的文字与 ActiveRecord 的属性翻译分开。 把给模型使用的语言文件放在名为 models 的文件夹,给视图使用的文字放在名为 views 的文件夹。
当使用额外目录的语言文件组织完成时,为了要载入这些目录,要在 application.rb 文件里描述这些目录。
1
2
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"
`u sers.show.title` 的数值能这样被 `a pp/views/users/show.html.haml` 查询:
在控制器与模型使用点分隔的键,来取代指定 :scope 选项。点分隔的调用更容易阅读及追踪层级。
1
2
3
4
5
I18n .t 'activerecord.errors.messages.record_invalid'
I18n .t :record_invalid , :scope => [:activerecord , :errors , :messages ]
Assets
利用这个 assets pipeline 来管理应用的结构。
保留 app/assets 给自定的样式表,Javascripts 或图片。
把自己开发,但不适合用在这个应用的函式库,放在 lib/assets/。
第三方代码如: jQuery 或 bootstrap 应放置在 vendor/assets。
当可能的时候,使用 gem 化的 assets 版本。(如: jquery-rails )。
Mailers
把 mails 命名为 SomethingMailer。 没有 Mailer 字根的话,不能立即显现哪个是一个 Mailer,以及哪个视图与它有关。
提供 HTML 与纯文本视图模版。
在你的开发环境启用信件失败发送错误。这些错误缺省是被停用的。
1
2
3
config.action_mailer.raise_delivery_errors = true
在开发模式使用 smtp.gmail.com 设置 SMTP 服务器(当然了,除非你自己有本地 SMTP 服务器)。
1
2
3
4
5
6
config.action_mailer.smtp_settings = {
address: 'smtp.gmail.com' ,
}
1
2
3
4
5
6
7
8
9
config.action_mailer.default_url_options = {host: "#{local_ip} :3000" }
config.action_mailer.default_url_options = {host: 'your_site.com' }
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
default from: 'Your Name <info@your_site.com>'
确定测试环境的 email 发送方法设置为 test :
1
2
3
config.action_mailer.delivery_method = :test
1
2
3
config.action_mailer.delivery_method = :smtp
当发送 HTML email 时,所有样式应为行内样式,由于某些用户有关于外部样式的问题。某种程度上这使得更难管理及造成代码重用。有两个相似的 gem 可以转换样式,以及将它们放在对应的 html 标签里: premailer-rails3 和 roadie 。
应避免页面产生响应时寄送 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
group :darwin do
gem 'rb-fsevent'
gem 'growl'
end
group :linux do
gem 'rb-inotify'
end
要在对的环境获得合适的 gem,添加以下代码至 `c onfig/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 清单。你应该在你的项目里避免使用它们。
这仍是一个完善中的清单。请告诉我受人欢迎但有缺陷的 gems 。
管理进程
测试 Rails 应用
也许 BDD 方法是实作一个新功能最好的方法。你从开始写一些高阶的测试(通常使用 Cucumber),然后使用这些测试来驱使你实作功能。一开始你给功能的视图写测试,并使用这些测试来创建相关的视图。之后,你创建丢给视图数据的控制器测试来实现控制器。最后你实作模型的测试以及模型自身。
Cucumber
用 @wip (工作进行中)标签标记你未完成的场景。这些场景不纳入考虑,且不标记为测试失败。当完成一个未完成场景且功能测试通过时,为了把此场景加至测试套件里,应该移除 @wip 标签。
配置你的缺省配置文件,排除掉标记为 @javascript 的场景。它们使用浏览器来测试,推荐停用它们来增加一般场景的执行速度。
替标记著 @javascript 的场景配置另一个配置文件。
配置文件可在 cucumber.yml 文件里配置。
1
2
profile_name: --tags @tag_name
1
cucumber -p profile_name
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
大量使用 descibe 及 context 。
如下地替 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
describe Article do
describe '#summary' do
end
describe '.latest' do
end
end
1
2
3
4
5
article = mock_model(Article )
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 ) }
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
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
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
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
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
class ArticlesHelper
def formatted_date(date)
end
end
= "Published at: #{formatted_date(@article .published_at)} "
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
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
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
Ruby describe Article do it ‘is valid with valid attributes’ do article.should be_valid end end
1
2
* 当测试验证时,使用 `h ave(x).errors_on` 来指定要被验证的属性。使用 `b e_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
* 在 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
* 我们如何测试上传器是否正确地调整大小。这里是一个 [carrierwave ](https://github.com/jnicklas/carrierwave ) 图片上传器的示例 spec:
Ruby
require 'spec_helper'
require 'carrierwave/test/matchers'
describe PersonAvatarUploader do
include CarrierWave::Test::Matchers
before (:all) do
UserAvatarUploader.enable_processing = true
end
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 风格的资源,若有闲暇时应当考虑延伸阅读: