TianShuai ′s Blog

Record my bit by bit...

Rails 风格指导

| Comments

感谢译者。

本页用于介绍 Ruby 社区首推的Rails代码编写风格,翻译来自: https://github.com/JuanitoFatas/rails-style-guide 序幕

风格是从伟大事物中分离出的美好事物。 
 Bozhidar Batsov

这份指南目的于演示一整套 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/ 下对应的文件)

    标记额外的资产给(如有任何)预编译:

    # config/environments/production.rb
    # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added)
    config.assets.precompile += %w( rails_admin/rails_admin.css rails_admin/rails_admin.js )

创立一个与生产环境相似的额外 staging 环境。

路由

当你需要加入一个或多个动作至一个 RESTful 资源时(你真的需要吗?),使用 member and collection 路由。
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
# 差
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 路由时,使用替代的区块语法。

resources :subscriptions do
  member do
    get 'unsubscribe'
    # 更多路由
  end
end

resources :photos do
  collection do
    get 'search'
    # 更多路由
  end
end
使用嵌套路由来更佳地表达与 ActiveRecord 模型的关系。
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
class Post < ActiveRecord::Base
  has_many :comments
end

class Comments < ActiveRecord::Base
  belongs_to :post
end

# routes.rb
resources :posts do
  resources :comments
end

使用命名空间路由来群组相关的行为。

namespace :admin do
  # Directs /admin/products/* to Admin::ProductsController
  # (app/controllers/admin/products_controller.rb)
  resources :products
end

不要使用合法的疯狂路由。这种路由会让每个控制器的动作透过 GET 请求存取。

# very bad
match ':controller(/:action(/:id(.:format)))'

控制器

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

模型

自由地引入不是 ActiveRecord 的类别吧。 Introduce non-ActiveRecord model classes freely.
替模型命名有意义(但简短)且不带缩写的名字。

如果你需要支援 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-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}$/i
  validates_length_of :content, :maximum => 500
end
更完整的示例,参考 RailsCast on the subject。

ActiveRecord

避免改动缺省的ActiveRecord(表的名字、主键,等等),除非你有一个非常好的理由(像是不受你控制的数据库)。
把宏风格的方法放在类别定义的前面(has_many, validates, 等等)。

偏好 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: /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i }
end

# 好
class EmailValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    record.errors[attribute] << (options[:message] || 'is not a valid email') unless value =~ /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
  end
end

class Person
  validates :email, email: true
end
所有惯用的验证器应放在一个共享的 gem 。

自由地使用命名的作用域。

当一个由 lambda 及参数定义的作用域变得过于复杂时,更好的方式是建一个作为同样用途的类别方法,并返回 ActiveRecord::Relation 对象。

注意 update_attribute 方法的行为。它不运行模型验证(不同于 update_attributes )并且可能把模型状态给搞砸。

使用用户友好的网址。在网址显示具描述性的模型属性,而不只是 id 。
有不止一种方法可以达成:

    覆写模型的 to_param 方法。这是 Rails 用来给对象建构网址的方法。缺省的实作会以字串形式返回该 id 的记录。它可被另一个人类可读的属性覆写。

    class Person
      def to_param
        "#{id} #{name}".parameterize
      end
    end

    为了要转换成对网址友好 (URL-friendly)的数值,字串应当调用 parameterize 。 对象的 id 要放在开头,以便给 ActiveRecord 的 find 方法查找。

    使用此 friendly_id gem。它允许藉由某些具描述性的模型属性,而不是用 id 来创建人类可读的网址。

    class Person
      extend FriendlyId
      friendly_id :name, use: :slugged
    end

    查看 gem 文档 获得更多关于使用的信息。

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] 的使用被视为相当常见的,你也可以考虑使用更罗嗦的(争议地可读性更高的) read_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
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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
# 过去的方式
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

    module ClientSideValidations::Middleware
      class Email < Base
        def response
          if request.params[:email] =~ /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
            self.status = 200
          else
            self.status = 404
          end
          super
        end
      end
    end

    建立一个新文件
    public/javascripts/rails.validations.custom.js.coffee 并在你的 application.js.coffee 文件加入一个它的参照:

    # app/assets/javascripts/application.js.coffee
    #= require rails.validations.custom

    添加你的用户端 validator

    #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 作用域:

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 文件里描述这些目录。

    # 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.

使用 "懒惰" 查询视图中使用的文字。假设我们有以下结构:

en:
  users:
    show:
      title: "User details page"

users.show.title 的数值能这样被 app/views/users/show.html.haml 查询:

= t '.title'

在控制器与模型使用点分隔的键,来取代指定 :scope 选项。点分隔的调用更容易阅读及追踪层级。

# 这样子调用使用
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, or 图片.
第三方代码如: jQuery  bootstrap 应放置在 vendor/assets
当可能的时候,使用 gem 化的 assets 版本。(如: jquery-rails).

Mailers

 mails 命名为 SomethingMailer 没有 Mailer 字根,不能立即显现哪个是一个 mailer,以及哪个视图与它有关。
提供 HTML 与纯文本视图模版。

在你的开发环境启用信件失败发送错误。这些错误缺省是被停用的。

# config/environments/development.rb

config.action_mailer.raise_delivery_errors = true

在开发模式使用 smtp.gmail.com 设置 SMTP 服务器(当然了,除非你自己有本地 SMTP 服务器)。

# config/environments/development.rb

config.action_mailer.smtp_settings = {
  address: 'smtp.gmail.com',
  # 更多设置
}

提供缺省的配置给主机名。

# 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 方法没有。

# 错误
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))

正确地显示寄与收件人地址的格式。使用下列格式:

# 在你的 mailer 类别
default from: 'Your Name <info@your_site.com>'

确定测试环境的 email 发送方法设置为 test 

# config/environments/test.rb

config.action_mailer.delivery_method = :test

开发与生产环境的发送方法应为 smtp 

# config/environments/development.rb, config/environments/production.rb

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 群组:

# Gemfile
group :darwin do
  gem 'rb-fsevent'
  gem 'growl'
end

group :linux do
  gem 'rb-inotify'
end

要在对的环境获得合适的 gem  添加以下代码至 config/application.rb 

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 介面以及更多东西。非常灵活且可客制。
capybara - Capybara 指在简化整合测试 Rack 应用的过程,像是 RailsSinatra  MerbCapybara 模拟了真实用户使用 web 应用的互动。 它与你测试在运行的驱动无关,并原生搭载 Rack::Test  Selenium 支持。透过外部 gem 支持 HtmlUnitWebKit  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 的精神领袖先驱。
faker - 实用的 gem 来产生仿造的数据(名字、地址,等等)。
feedzirra - 非常快速及灵活的 RSS/Atom 种子解析器。
friendly_id - 透过使用某些具描述性的模型属性,而不是使用 id,允许你创建人类可读的网址。
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,它消耗大量的内存。我会推荐使用 Mustang 来取代。

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

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

测试 Rails 应用

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

 @wip (工作进行中)标签标记你未完成的场景。这些场景不纳入考虑,且不标记为测试失败。当完成一个未完成场景且功能测试通过时,为了把此场景加至测试套件里,应该移除 @wip 标签。
配置你的缺省配置文件,排除掉标记为 @javascript 的场景。它们使用浏览器来测试,推荐停用它们来增加一般场景的执行速度。

替标记著 @javascript 的场景配置另一个配置文件。

    配置文件可在 cucumber.yml 文件里配置。

    # 配置文件的定义:
    profile_name: --tags @tag_name

    带指令运行一个配置文件:

    cucumber -p profile_name

若使用 fabrication 来替换假数据 (fixtures),使用预定义的 fabrication steps

不要使用旧版的 web_steps.rb 步骤定义! 最新版 Cucumber 已移除 web steps ,使用它们导致冗赘的场景,而且它并没有正确地反映出应用的领域。

当检查一元素的可视文字时,检查元素的文字而不是检查 id。这样可以查出 i18n 的问题。

给同种类对象创建不同的功能特色:

# 差
Feature: Articles
# ... 特色实作 ...

# 好
Feature: Article Editing
# ... 特色实作 ...

Feature: Article Publishing
# ... 特色实作 ...

Feature: Article Search
# ... 特色实作 ...

每一个特色有三个主要成分:
    Title
    Narrative - 简短说明这个特色关于什么。
    Acceptance criteria - 每个由独立步骤组成的一套场景。

最常见的格式称为 Connextra 格式。

In order to [benefit] ...
A [stakeholder]...
Wants to [feature] ...

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

自由地使用场景概述使你的场景备作它用 (keep your scenarios DRY)

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)

使用多行步骤参数来避免重复

场景: 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)

# ...
When I subscribe for news from the category "Technical News"
# ...

# the step:
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

一个例子仅用一个期望值。

# 差
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"

class Article
  def summary
    #...
  end

  def self.latest
    #...
  end
end

# the spec...
describe Article
  describe '#summary'
    #...
  end

  describe '.latest'
    #...
  end
end

使用 fabricators 来创建测试对象。

大量使用 mocks  stubs

# mocking 一个模型
article = mock_model(Article)

# stubbing a method
Article.stub(:find).with(article.id).and_return(article)

 mocking 一个模型时,使用 as_null_object 方法。它告诉输出仅监听我们预期的讯息,并忽略其它的讯息。

article = mock_model(Article).as_null_object

使用 let 区块而不是 before(:all) 区块替 spec 例子创建数据。let 区块会被懒惰求值。

# 使用这个:
let(:article) { Fabricate(:article) }

# ... 而不是这个:
before(:each) { @article = Fabricate(:article) }

当可能时,使用 subject 

describe Article do
  subject { Fabricate(:article) }

  it 'is not published on creation' do
    subject.should_not be_published
  end
end

如果可能的话,使用 specify 。它是 it 的同义词,但在没 docstring 的情况下可读性更高。

# 差
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 

# 差
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

视图

视图测试的目录结构要与 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 方法提供由控制器提供视图使用的实例变数。

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
偏好 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
当一个视图使用 helper 方法时,这些方法需要被 stubbedStubbing 这些 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.html' do
  it 'displays the formatted date of article publishing'
    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
11
 spec/helpers 目录的 helper specs 是与视图 specs 分开的。

控制器

Mock 模型及 stub 他们的方法。测试控制器时不应依赖建模。

仅测试控制器需负责的行为:
    执行特定的方法
    从动作返回的数据 - assigns, 等等。

    从动作返回的结果 - template render, redirect, 等等。

ruby

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

    describe ArticlesController do
      # The model will be used in the specs for all methods of the controller
      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
3
当控制器根据不同参数有不同行为时,使用 context

# 一个在控制器中使用 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
  let(:article) { Fabricate(:article) }
end

加入一个例子确保捏造的模型是可行的。 Add an example ensuring that the fabricated model is valid.

describe Article
  it 'is valid with valid attributes' do
    article.should be_valid
  end
end
1
当测试验证时,使用 have(x).errors_on 来指定要被验证的属性。使用 be_valid 不保证问题在目的的属性。

ruby

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

# 偏好
describe '#title'
  it 'is required' do
    article.title = nil
    article.should have(1).error_on(:title)
  end
end
1
2
3
4
5
6
7
8
9
10
11
12
替每个有验证的属性加另一个 describe

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

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

ruby

describe Article
  describe '#title'
    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'
     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
Uploaders

    我们如何测试上传器是否正确地调整大小。这里是一个 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
```
原文转自: http://www.cnblogs.com/hedgehog-ZDH/archive/2012/11/16/2773749.html

Comments