序幕
Role models are important.
-- 機械戰警 Alex J. Murphy
這份指南目的於演示一整套 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 # 預編譯額外的資產(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
andcollection
路由。# 差 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)。resources :subscriptions do member do get 'unsubscribe' # 更多路由 end end resources :photos do collection do get 'search' # 更多路由 end end
-
使用嵌套路由(nested routes)來更佳地表達與 ActiveRecord 模型的關係。
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
-
不要在控制器裏使用留給後人般的瘋狂路由(legacy wild controller route)。這種路由會讓每個控制器的動作透過 GET 請求存取。
# 非常差 match ':controller(/:action(/:id(.:format)))'
控制器
- 讓你的控制器保持苗條 ― 它們應該只替視圖層取出數據且不包含任何業務邏輯(所有業務邏輯應當放在模型裏)。
- 每個控制器的行動應當(理想上)只調用一個除了初始的 find 或 new 方法。
- 控制器與視圖之間共享不超過兩個實例變量(instance variable)。
模型
- 自由地引入不是 ActiveRecord 的類別吧。
- 替模型命名有意義(但簡短)且不帶縮寫的名字。
-
如果你需要模型有著 ActiveRecord 行爲的對象,比方說驗證這一塊,使用 ActiveAttr gem。
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。
ActiveRecord
- 避免改動缺省的 ActiveRecord(表的名字、主鍵,等等),除非你有一個非常好的理由(像是不受你控制的數據庫)。
-
把宏風格的方法(
has_many
,validates
, 等等)放在類別定義的前面。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 模型有附加的屬性及驗證# 使用 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 文件。
# 差 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)。
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
裏來惰性地初始化。# 差勁 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
對象。你也可以這麼定義出更精簡的作用域。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
的記錄。它可被另一個具人類可讀的屬性覆寫。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
。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
方法,並移除擴展的部份。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。 -
避免在表裏設置缺省數據。使用模型層來取代。
def amount self[:amount] or 0 end
然而
self[:attr_name]
的使用被視爲相當常見的,你也可以考慮使用更羅嗦的(爭議地可讀性更高的)read_attribute
來取代:def amount read_attribute(:amount) or 0 end
-
當編寫建設性的遷移時(加入表或欄位),使用 Rails 3.1 的新方式來遷移 - 使用
change
方法取代up
與down
方法。# 過去的方式 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
而來的自定 validatormodule 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
文件加入一個它的參照:```Ruby # 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 或圖片。 - 把自己開發,但不適合用在這個應用的函式庫,放在
lib/assets/
。 - 第三方代碼如: 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 界面以及更多東西。非常靈活且可客製化。
- 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
文件裏配置。# 配置文件的定義: 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 "[email protected]" 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" # ... # 步驟: 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 do describe '#summary' do #... end describe '.latest' do #... end end
-
使用 fabricators 來創建測試對象。
-
大量使用 mocks 與 stubs。
# mocking 一個模型 article = mock_model(Article) # stubbing 一個方法 Article.stub(:find).with(article.id).and_return(article)
-
當 mocking 一個模型時,使用
as_null_object
方法。它告訴輸出僅監聽我們預期的訊息,並忽略其它的訊息。article = mock_model(Article).as_null_object
-
使用
let
區塊而不是before(:each)
區塊替 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
-
Use
shared_examples
if you want to create a spec group that can be shared by many other tests.# 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
- 永遠在視圖測試來 mock 模型。視圖的目的只是顯示信息。
assign
方法提供由控制器提供視圖使用的實例變量(instance variable)。
# 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
- 偏好 capybara 否定情況選擇器,勝於搭配正面情況的 should_not 。
# 差 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')
- 當一個視圖使用 helper 方法時,這些方法需要被 stubbed。Stubbing 這些 helper 方法是在
template
完成的:
# 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
- 在
spec/helpers
目錄的 helper specs 是與視圖 specs 分開的。
控制器
- Mock 模型及 stub 他們的方法。測試控制器時不應依賴建模。
- 僅測試控制器需負責的行爲:
- 執行特定的方法
- 從動作返回的數據 - assigns, 等等。
-
從動作返回的結果 - template render, redirect, 等等。
# 常用的控制器 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
-
當控制器根據不同參數有不同行爲時,使用 context。
# 一個在控制器中使用 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
模型
- 不要在自己的測試裏 mock 模型。
- 使用捏造的東西來創建真的對象
- Mock 別的模型或子對象是可接受的。
- 在測試裏建立所有例子的模型來避免重複。
describe Article do let(:article) { Fabricate(:article) } end
- 加入一個例子確保捏造的模型是可行的。
describe Article do it 'is valid with valid attributes' do article.should be_valid end end
- 當測試驗證時,使用
have(x).errors_on
來指定要被驗證的屬性。使用be_valid
不保證問題在目的的屬性。
# 差 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
- 替每個有驗證的屬性加另一個
describe
。
describe Article do describe '#title' do it 'is required' do article.title = nil article.should have(1).error_on(:title) end end end
- 當測試模型屬性的獨立性時,把其它對象命名爲
another_object
。
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
Mailers
- 在 Mailer 測試的模型應該要被 mock。 Mailer 不應依賴建模。
- Mailer 的測試應該確認如下:
- 這個 subject 是正確的
- 這個 receiver e-mail 是正確的
- 這個 e-mail 寄送至對的郵件地址
-
這個 e-mail 包含了需要的信息
describe SubscriberMailer let(:subscriber) { mock_model(Subscription, email: '[email protected]', 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
Uploaders
- 我們如何測試上傳器是否正確地調整大小。這裏是一個 carrierwave 圖片上傳器的示例 spec:
# 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 風格的資源,若有閒暇時應當考慮延伸閱讀:
貢獻
在本指南所寫的每個東西都不是定案。這只是我渴望想與同樣對 Rails 編碼風格有興趣的大家一起工作,以致於最終我們可以替整個 Ruby 社羣創造一個有益的資源。
歡迎開票或發送一個帶有改進的更新請求。在此提前感謝你的幫助!
授權
This work is licensed under a Creative Commons Attribution 3.0 Unported License
口耳相傳
一份社羣驅動的風格指南,對一個社羣來說,只是讓人知道有這個社羣。微博轉發這份指南,分享給你的朋友或同事。我們得到的每個註解、建議或意見都可以讓這份指南變得更好一點。而我們想要擁有的是最好的指南,不是嗎?
共勉之,
Bozhidar