簡介

Ruby Taiwan : ) Ruby Taiwan

關於作者

這篇文章翻譯自 bbatsov 所寫的 Ruby Style GuideRails Style Guide

關於譯者

Juanito Fatas,你可以在 twitter、ruby-taiwan 找到我。

不好意思,我剛學習 Ruby 及Rails,翻譯的品質可能不太好,

有錯誤不好的地方,麻煩去 github 開任務給我。

提交建議

Ruby Style Guide 建議

Rails Style Guide 建議

為什麼翻譯此文

幾個禮拜前我開始學習 Ruby 及 Rails。我剛好看到了這兩個指導,我覺得這可以讓我有全觀的理解及避免可能犯的錯誤,這樣子再去學習會更有方向,這是我翻譯的原因。

而關於風格,我看過一本書,有一段文字,詮釋的很好:

Style is necessary only where understanding is missing. A corollary to this is that sometimes the only way to effectively use something you don't understand is to copy styles observed elsewhere. – From Let Over Lambda

我希望你讀的愉快。

內容許可

內容採用創用 CC 授權釋出

Creative Commons License

Ruby 風格指南

序幕

作為 Ruby 開發者,有一件總是令我煩心的事 — Python 開發者有一份好的程式風格參考指南(PEP-8) 而我們永遠沒有一份官方指南,一份記錄 Ruby 程式風格及最佳實踐的指南。而我們確信風格很重要。我也相信這些好傢伙們,像我們是 Ruby 開發者,應該可以自己產生一份這夢寐以求的文件。

這份指南開始是作為我們公司內部 Ruby 程式指南(由我所寫的)。進行到某個部分時,我決定要把我的成果貢獻給廣大的 Ruby 社群,而且這個世界需要來自另一個公司內部的一點幫助。然而這個世界也可以從由社群制定及驅動的一系列 Ruby 程式慣例、實踐及風格中受益。

在開始寫這份指南時,我收到世界上很多優秀 Ruby 社群用戶們的反饋。感謝所有的建議及幫助!我們同心協力創造一個能夠讓每一個 Ruby 開發者受益的資源。

順道一提,如果你對 Rails 有興趣,你可以看看這份與之互補的 Ruby on Rails 3 風格指南

Ruby 風格指南

這份 Ruby 風格指南向你推薦現實世界中,Ruby 程式設計師如何寫出可被別的 Ruby 程式設計師維護的程式碼。一份風格指南,反映出現實世界中的用法,並有一個理想,幫助人們避免使用危險的程式碼 — 不管它看起來有多好。

本指南分成數個相關規則的小節。我試著在每個規則後說明理由(如果省略的話,我相信理由相當明顯)。

我沒有想到所有的規則 — 他們大致上是基於,我作為一個專業軟體工程師的廣泛生涯,從 Ruby 社群成員所得到的反饋及建議,和數個高度評價的 Ruby 程式設計資源,像是 "Programming Ruby 1.9" 以及 "The Ruby Programming Language"

本指南仍在進行改善中 — 某些規則缺乏實例,某些規則沒有例子來清楚地展示它們。在最後交付時,這些議題會被解決 — 就先把它們記在心理吧。

你可以使用 Transmuter 來產生本指南的一份 PDF 或 HTML 副本。

原始碼排版

使用 UTF-8 作為原始檔案的編碼。 每個縮排層級使用兩個空格

# 好
def some_method
  do_something
end

# 不好 - 四個空格
def some_method
    do_something
end

使用 Unix 風格的行編碼 (預設包含 BSD/Solaris/Linux/OSX 的使用者,Windows 使用者要特別小心。) 如果你使用 Git ,你也許會想加入下面這個配置設定,來保護你的專案被 Windows 的行編碼侵入:

$ git config --global core.autocrlf true

使用空格來圍繞運算元,逗點 , 、冒號 : 及分號 ; 之後,圍繞 {} 之前。 空格可能對(大部分)Ruby 直譯器來說是無關緊要的,但正確的使用是寫出可讀性高的程式碼的關鍵。

sum = 1 + 2
a, b = 1, 2
1 > 2 ? true : false; puts 'Hi'
[1, 2, 3].each { |e| puts e }

唯一的例外是當使用指數運算元時:

# 不好
e = M * c ** 2

# 好
e = M * c**2

不要有空格在 ([ 之後,或 ]) 之前。

some(arg).other
[1, 2, 3].length

whencase 縮排在同一層。我知道很多人不同意這一點,但這是“The Ruby Programming Language”及“Programming Ruby”所設立的風格。

case
when song.name == 'Misty'
  puts 'Not again!'
when song.duration > 120
  puts 'Too long!'
when Time.now.hour > 21
  puts "It's too late"
else
  song.play
end

kind = case year
       when 1850..1889 then 'Blues'
       when 1890..1909 then 'Ragtime'
       when 1910..1929 then 'New Orleans Jazz'
       when 1930..1939 then 'Swing'
       when 1940..1950 then 'Bebop'
       else 'Jazz'
       end

def 之間使用空行,並且把方法分成合乎邏輯的段落。

def some_method
  data = initialize(options)

  data.manipulate!

  data.result
end

def some_method
  result
end

當一個方法呼叫的參數擴展至多行時,排列它們。

# 一開始(一行太長)
def send_mail(source)
  Mailer.deliver(to: 'bob@example.com', from: 'us@example.com', subject: 'Important message', body: source.text)
end

# 不好(一般的縮排)
def send_mail(source)
  Mailer.deliver(
    to: 'bob@example.com',
    from: 'us@example.com',
    subject: 'Important message',
    body: source.text)
end

# 不好(兩倍縮排)
def send_mail(source)
  Mailer.deliver(
      to: 'bob@example.com',
      from: 'us@example.com',
      subject: 'Important message',
      body: source.text)
end

# 好
def send_mail(source)
  Mailer.deliver(to: 'bob@example.com',
                 from: 'us@example.com',
                 subject: 'Important message',
                 body: source.text)
end

使用 RDoc 以及它的慣例來撰寫 API 文件。不要在註解區塊及 def 之前放一個空行。 讓每一行保持在少於 80 個字元。 避免尾隨的空白(trailing whitesapce)。

語法

使用 def 時,當有參數時使用括號。當方法不接受任何參數時,省略括號。

def some_method
# 省略主體
end

def some_method_with_arguments(arg1, arg2)
# 省略主體
end

永遠不要使用 for ,除非你很清楚為什麼。大部分情況應該使用迭代器來取代。for 是由 each 所實作的(所以你加入了一層的迂迴),但出乎意料的是 — for 並沒有包含一個新的視野 (不像是 each)而在這個區塊中定義的變數將會被外部所看到。

arr = [1, 2, 3]

# 不好
for elem in arr do
  puts elem
end

# 好
arr.each { |elem| puts elem }

永遠不要在多行的 if/unless 使用 then

# 不好
if some_condition then
  # 省略主體
end

# 好
if some_condition
  # 省略主體
end

偏愛三元運算元 ? : 勝於 if/then/else/end 結構。它更為常見及更精準。

# 不好
result = if some_condition then something else something_else end

# 好
result = some_condition ? something : something_else

使用一個表達式給一個三元運算元的分支。這也意味著三元運算符不要寫成巢狀式。巢狀情況使用 if/else 結構。

# 不好
some_condition ? (nested_condition ? nested_something : nested_something_else) : something_else

# 好
if some_condition
  nested_condition ? nested_something : nested_something_else
else
  something_else
end

永遠不要使用 if x: ... — 它已經在 Ruby 1.9 被移除了。使用三元運算元來取代。

# 不好
result = if some_condition: something else something_else end

# 好
result = some_condition ? something : something_else

永遠不要使用 if x; ... 使用三元運算元來取代。

一行的情況使用 when x then ... 。替代方案的語法 when x: ... 在 Ruby 1.9 被移除了。

永遠不要使用 when x; ... 。參考前一個規則。

布林表達式使用 &&/||,控制流程使用 and/or

(經驗法則:如果你需要使用外部括號,你正在使用錯誤的運算元。)

# 布林表達式
if some_condition && some_other_condition
  do_something
end

# 控制流程
document.saved? or document.save!

避免多行的 ? :(三元運算元),使用 if/unless 來取代。

當你有單行的主體時,偏愛 if/unless 修飾符。

另一個好的方法是使用控制流程的 and/or

# 不好
if some_condition
  do_something
end

# 好
do_something if some_condition

# 另一個好方法
some_condition and do_something

否定條件偏愛 unless 優於 if (或是控制流程 or)。

# 不好
do_something if !some_condition

# 好
do_something unless some_condition

# 另一個好方法
some_condition or do_something

永遠不要使用 unless 搭配 else。 將它們改寫成肯定條件。

# 不好
unless success?
  puts 'failure'
else
  puts 'success'
end

# 好
if success?
  puts 'success'
else
  puts 'failure'
end

不要使用括號圍繞 if/unless/while 的條件式,除非這條件包含了一個賦值(見下面使用 = (一個賦值)的回傳值)。

# 不好
if (x > 10)
  # 省略主體
end

# 好
if x > 10
  # 省略主體
end

# 好
if (x = self.next_value)
  # 省略主體
end

忽略圍繞方法參數的括號,如內部 DSL (如:Rake, Rails, RSpec),Ruby 中帶有“關鍵字”狀態的方法(如:attr_reader, puts)以及屬性存取方法。

所有其他的方法呼叫,使用括號圍繞參數。

class Person
  attr_reader :name, :age

  # 忽略
end

temperance = Person.new('Temperance', 30)
temperance.name

puts temperance.age

x = Math.sin(y)
array.delete(e)

單行區塊喜好 {...} 勝於 do..end。多行區塊避免使用 {...}(多行串連總是醜陋)。在 do...end 、“控制流程”及“方法定義”,永遠使用 do...end (如 Rakefile 及某些 DSL)。串連時避免使用 do...end

names = ["Bozhidar", "Steve", "Sarah"]

# 好
names.each { |name| puts name }

# 不好
names.each do |name|
  puts name
end

# 好
names.select { |name| name.start_with?("S") }.map { |name| name.upcase }

# 不好
names.select do |name|
  name.start_with?("S")
end.map { |name| name.upcase }
某些人會爭論多行串連時,使用 {...} 看起來還可以,
但他們應該問問自己 — 這樣程式碼真的可讀嗎
以及不能把區塊內容取出來放到絕妙的方法中嗎。

避免在不需要的場合時使用 return

# 不好
def some_method(some_arr)
  return some_arr.size
end

# 好
def some_method(some_arr)
  some_arr.size
end

當賦予預設值給方法參數時,使用空格圍繞 = 運算元。

# 不好
def some_method(arg1=:default, arg2=nil, arg3=[])
  # 做些事情...
end

# 好
def some_method(arg1 = :default, arg2 = nil, arg3 = [])
  # 做些事情...
end

然而幾本 Ruby 書建議第一個風格,
第二個風格在實踐中更為常見(並可爭議地可讀性更高一點)。

避免在不需要的場合使用續行 \ 。在實踐中,盡量避免使用續行。

# 不好
result = 1 - \
         2

# 好 (但仍然醜的跟地獄一樣)
result = 1 \
         - 2

使用 =(一個賦值)的回傳值是好的,但用括號環繞賦值。

# 好 — 演示賦值的目標用途
if (v = array.grep(/foo/)) ...

# 不好
if v = array.grep(/foo/) ...

# 也很好 — 演示賦值的目標用途及有正確的優先順序
if (v = self.next_value) == "hello" ...

隨意使用 ||= 來初始化變數

# 僅在name為nil或false時,把名字設為 Bozhidar。
name ||= 'Bozhidar'

不要使用 ||= 來初始化布林變數。

(想看看如果現在的值剛好是 false 時會發生什麼。)

# 不好 — 會把 enabled 設成真,即便它本來是假。
enabled ||= true

# 好
enabled = true if enabled.nil?

避免使用 Perl 風格的特別變數(像是 $0-9, $`, 等等)。它們看起來非常神祕以及不鼓勵使用一行的腳本。

避免在方法名與左括號之間放一個空格。

# 不好
f (3 + 2) + 1

# 好
f(3 + 2) + 1

如果方法的第一個參數由左括號開始,永遠在這個方法呼叫裡使用括號。

舉個例子: f((3+2) + 1)

總是使用 -w 來執行 Ruby 直譯器,如果你忘了某個上述的規則,它就會警告你!

當你的雜湊鍵是符號時,使用 Ruby 1.9 雜湊字面語法。

# 不好
hash = { :one => 1, :two => 2 }

# 好
hash = { one: 1, two: 2 }

使用新的 lambda 字面語法。

# 不好
lambda = lambda { |a, b| a + b }
lambda.call(1, 2)

# 好
lambda = ->(a, b) { a + b }
lambda.(1, 2)

未使用的區塊參數使用 _

# 不好
result = hash.map { |k, v| v + 1 }

# 好
result = hash.map { |_, v| v + 1 }

命名

方法與變數使用蛇底式小寫( snake_case)。

類別與模組使用駝峰式大小寫( CamelCase )。(保留像是 HTTP、RFC、XML 這種縮寫為大寫)

其他常數使用尖叫蛇底式大寫( SCREAMING_SNAKE__CASE )。

判斷式(predicate)方法的名字(回傳布林值的方法)應以問號結尾。(即 Array#empty? )

有潛在“危險性”的方法,若此危險方法有安全版本存在時,應以驚嘆號結尾(即:改動 self 或參數、 exit! 等等方法)。

# 不好 - 沒有對應的安全方法
class Person
  def update!
  end
end

# 好
class Person
  def update
  end
end

# 好
class Person
  def update!
  end

  def update
  end
end

如果可能的話,從危險方法(bang)的角度來定義對應的安全方法(non-bang)。

class Array
  def flatten_once!
    res = []

    each do |e|
      [*e].each { |f| res << f }
    end

    replace(res)
  end

  def flatten_once
    dup.flatten_once!
  end
end

在短的區塊使用 reduce 時,把參數命名為 |a, e| (累加器,元素)。

當定義二元運算元時,把參數命名為 other

def +(other)
  # 省略主體
end

偏好 map 勝於 collectfind 勝於 detectselect 勝於 find_allreduce 勝於 inject 以及 size 勝於 length 。這不是一個硬性要求;如果使用別名增加了可讀性,使用它沒關係。

這些有押韻的方法名是從 Smalltalk 繼承而來,在別的語言不常見。鼓勵使用 select 而不是 find_all 的理由是它跟 reject 搭配起來是一目了然的。

註解 (Comment)

撰寫自我記錄的程式碼並忽略之後的小節。我是認真的!

比一個單字長的註解要大寫及使用標點符號。

句號後使用一個空格

避免多餘的註解

# 不好
counter += 1 # 把計數器加一

保持現有的註解是最新的。過時的註解比沒有註解還差。

避免替爛程式碼寫註解。重構程式碼讓它們看起來一目了然。

(要嘛就做,要嘛不做 ― 不要只是試試看。)

註釋 (Annotation)

註釋應該直接寫在相關程式碼那行之後。

註釋關鍵字後方伴隨著一個冒號及空白,接著一個描述問題的記錄。

如果需要用多行來描述問題,之後的行要放在 # 號後面並縮排兩個空白。

def bar
  # FIXME: 這在 v3.2.1 版本之後會異常當掉,或許與
  #   BarBazUtil 的版本更新有關
  baz(:quux)
end

在問題是顯而易見的情況下,任何的文件會是多餘的,註釋應該要留在可能有問題的那行。這個用法是例外而不是規則。

def bar
  sleep 100 # OPTIMIZE
end

使用 TODO 來標記之後應被加入的未實現功能或特色。

使用 FIXME 來標記一個需要修復的程式碼。

使用 OPTIMIZE 來標記可能影響效能的緩慢或效率低落的程式碼。

使用 HACK 來標記代碼異味,其中包含了可疑的編碼實踐以及應該需要重構。

使用 REVIEW 來標記任何需要審視及確認正常動作的地方。

舉例來說:

REVIEW: 我們確定用戶現在是這麼做的嗎?

如果你覺得適當的話,使用其他你習慣的註釋關鍵字,但記得把它們記錄在專案的 README 或類似的地方。

類別

當設計類別階層時,確認它們符合 Liskov 代換原則。 盡可能讓你的類別越堅固越好。 永遠替類別提供一個適當的 to_s 方法來表示領域模型(domain model)。

class Person
  attr_reader :first_name, :last_name

  def initialize(first_name, last_name)
    @first_name = first_name
    @last_name = last_name
  end

  def to_s
    "#@first_name #@last_name"
  end
end

使用 attr 這類函數來定義瑣碎的 accessor 或 mutators。

# 不好
class Person
  def initialize(first_name, last_name)
    @first_name = first_name
    @last_name = last_name
  end

  def first_name
    @first_name
  end

  def last_name
    @last_name
  end
end

# 好
class Person
  attr_reader :first_name, :last_name

  def initialize(first_name, last_name)
    @first_name = first_name
    @last_name = last_name
  end
end

考慮使用 Struct.new,它替你定義了那些瑣碎的存取器(accessors),建構式(constructor)以及比較運算元(comparison operators)。

# 好
class Person
  attr_reader :first_name, :last_name

  def initialize(first_name, last_name)
    @first_name = first_name
    @last_name = last_name
  end
end

# 較佳
class Person < Struct.new (:first_name, :last_name)
end

考慮加入工廠方法來提供額外合理的方式,來創造一個特定類別的實體。

class Person
  def self.create(options_hash)
    # 省略主體
  end
end

偏好鴨子類型勝於繼承。

# 不好
class Animal
  # 抽象方法
  def speak
  end
end

# 繼承高層次的類別 (superclass)
class Duck < Animal
  def speak
    puts 'Quack! Quack'
  end
end

# 繼承高層次的類別 (superclass)
class Dog < Animal
  def speak
    puts 'Bau! Bau!'
  end
end

# 好
class Duck
  def speak
    puts 'Quack! Quack'
  end
end

class Dog
  def speak
    puts 'Bau! Bau!'
  end
end

由於繼承中“討厭的”行為,避免使用類別變數 (@@)。

class Parent
  @@class_var = 'parent'

  def self.print_class_var
    puts @@class_var
  end
end

class Child < Parent
  @@class_var = 'child'
end

Parent.print_class_var # => will print "child"

如同你所看到的,在類別階級中的所有類別其實都共享一個類別變數。
應該通常偏好使用實體變數而不是類別變數。

依據方法的目的用途指定適當的可視層級 (private , protected )。

別把所有方法都設為 public (方法的預設值)。

我們現在是在寫 Ruby ,不是 Python

public, protected, private 和方法定義有一樣的縮排。

在每一個上方留一個空行。

class SomeClass
  def public_method
    # ...
  end

  private
  def private_method
    # ...
  end
end

使用 def self.method 來定義 singleton 方法。這讓方法更能抵抗重構帶來的變化。

class TestClass
  # 不好
  def TestClass.some_method
    # 省略主體
  end

  # 好
  def self.some_other_method
    # 省略主體
  end

  # 也有可能且當你要定義多個
  # singleton時的便利方法
  class << self
    def first_method
      # 省略主體
    end

    def second_method_etc
      # 省略主體
    end
  end
end

異常

不要封鎖異常。

begin
  # 這裡發生了一個異常
rescue SomeError
  # 救援子句完全沒有做事
end

不要為了控制流程而使用異常。

# 不好
begin
  n / d
rescue ZeroDivisionError
  puts "Cannot divide by 0!"
end

# 好
if d.zero?
  puts "Cannot divide by 0!"
else
  n / d
end

避免救援 Exception 類別。這會把信號困住,並呼叫 exit,導致你需要 kill -9 進程。

# 不好
begin
  # 呼叫 exit 及殺掉信號會被捕捉(除了 kill -9)
  exit
rescue Exception
  puts "you didn't really want to exit, right?"
  # 異常處理
end

# 好
begin
  # 從 StandardError 中救援一個救援子句,
  # 不是許多程式設計師所假定的異常。
rescue => e
  # 異常處理
end

# 也很好
begin
  # 這裡發生一個異常

rescue StandardError => e
  # 異常處理
end

把較具體的異常放在救援串連的較上層,不然它們永遠不會被救援。

# 不好
begin
  # 一些程式碼
rescue Exception => e
  # 一些處理
rescue StandardError => e
  # 一些處理
end

# 好
begin
  # 一些程式碼
rescue StandardError => e
  # 一些處理
rescue Exception => e
  # 一些處理
end

在 ensure 區塊中釋放你程式的外部資源。

f = File.open("testfile")
begin
  # .. 處理
rescue
  # .. 錯誤處理
ensure
  f.close unless f.nil?
end

偏愛使用標準函式庫的異常處理勝於導入新的異常類別。

集合

當你需要使用一個字串的陣列時,偏好使用 %w 的字面陣列語法。

# 不好
STATES = ['draft', 'open', 'closed']

# 好
STATES = %w(draft open closed)

避免在陣列中創造巨大的間隔。

arr = []
arr[100] = 1 # 現在你有一個很多 nil 的陣列

當處理獨一無二的元素時,使用 Set 來替代 ArraySet 實現了不重複的無序數值集合。Set 是陣列直觀的內部操作功能與雜湊的快速存取的混合體。 使用符號取代字串作為雜湊鍵。

# 不好
hash = { 'one' => 1, 'two' => 2, 'three' => 3 }

# 好
hash = { one: 1, two: 2, three: 3 }

避免使用可變的物件作為鍵值。

優先使用新的 1.9 字面雜湊語法而不是 => (hashrocket) 語法。

# 不好
hash = { :one => 1, :two => 2, :three => 3 }

# 好
hash = { one: 1, two: 2, three: 3 }

相信這個事實吧, 1.9 的雜湊是有序的。

在遍歷一個集合時,不要改動它。

字串

偏好字串插值 (interpolation),而不是字串串接 (concatenation)。

# 不好
email_with_name = user.name + ' <' + user.email + '>'

# 好
email_with_name = "#{user.name} <#{user.email}>"

當你不需要插入特殊符號如 \t, \n, ', 等等時,偏好單引號的字串。

# 不好
name = "Bozhidar"

# 好
name = 'Bozhidar'

不要使用 {} 圍繞要被插入字串的實體變數。

class Person
  attr_reader :first_name, :last_name

  def initialize(first_name, last_name)
    @first_name = first_name
    @last_name = last_name
  end

  # 不好
  def to_s
    "#{@first_name} #{@last_name}"
  end

  # 好
  def to_s
    "#@first_name #@last_name"
  end
end

當你需要建構龐大的資料區段(chunk)時,避免使用 String#+ 。使用 String#<< 來替代。字串串接在對的地方改變字串實體,並且永遠比 String#+ 來得快,String#+ 創造了一堆新的字串物件。

# 好也比較快
html = ''
html << '<h1>Page title</h1>'

paragraphs.each do |paragraph|
  html << "<p>#{paragraph}</p>"
end

正規表示法

如果你只需要在字串中簡單的搜索文字,不要使用正規表示法:string['text']

針對簡單的字串查詢,你可以直接在字串索引中直接使用正規表示法。

match = string[/regexp/]             # 獲得匹配正規表示法的內容
first_group = string[/text(grp)/, 1] # 或得分組的內容
string[/text (grp)/, 1] = 'replace'  # string => 'text replace'

當你不需要替結果分組時,使用非分組的群組。

/(first|second)/   # 不好
/(?:first|second)/ # 好

避免使用 $1-9,因為它們很難追蹤它們包含什麼。可以使用命名群組來替代。

# 不好
/(regexp)/ =~ string
...
process $1

# 好
/(?<meaningful_var>regexp)/ =~ string
...
process meaningful_var

字元類別只有幾個你需要關心的特殊字元:^, -, \, ],所以你不用逃脫字元 . 或在 [] 的中括號。 小心使用 ^$ ,它們匹配的是一行的開始與結束,不是字串的開始與結束。如果你想要匹配整個字串,使用 \A\z

(譯註:\Z 實為 /\n?\z/,使用 \z 才能匹配到有含新行的字串的結束)

string = "some injection\nusername"
string[/^username$/]   # 匹配
string[/\Ausername\z/] # 無匹配

針對複雜的正規表示法,使用 x 修飾符。這讓它們的可讀性更高並且你可以加入有用的註解。只是要小心忽略的空白。

regexp = %r{
  start         # 一些文字
  \s            # 空白字元
  (group)       # 第一組
  (?:alt1|alt2) # 一些替代方案
  end
}x

針對複雜的替換,subgsub 可以與區塊或雜湊來使用。

百分比字面

隨意使用 %w

STATES = %w(draft open closed)

使用 %() 給需要插值與嵌入雙引號的單行字串。多行字串,偏好使用 heredocs 。

# 不好(不需要插值)
%(<div class="text">Some text</div>)
# 應該使用 '<div class="text">Some text</div>'

# 不好(沒有雙引號)
%(This is #{quality} style)
# 應該使用 "This is #{quality} style"

# 不好(多行)
%(<div>\n<span class="big">#{exclamation}</span>\n</div>)
# 應該是一個 heredoc

# 好(需要插值、有雙引號以及單行)
%(<tr><td class="name">#{name}</td>)

正規表示法要匹配多於一個的 / 字元時,使用 %r

# 不好
%r(\s+)

# 仍不好
%r(^/(.*)$)
# 應當是 /^\/(.*)$/

# 好
%r(^/blog/2011/(.*)$)

避免 %q, %Q, %x, %s 以及 %W。 偏好 () 作為所有 % 字面的分隔符。

元程式設計

寫一個函式庫時不要在核心類別搗亂(不要替它們加 monkey patch)

偏好區塊形式的 class_eval 勝於字串插值 (string-interpolated)的形式。

當你使用字串插值形式時,總是提供 __FILE____LINE__,使你的 backtrace 看起來有意義:

class_eval "def use_relative_model_naming?; true; end", __FILE__, __LINE__

偏好 define_method 勝於 class_eval{ def ... }

當使用 class_eval (或其它的 eval)搭配字串插值時,添加一個註解區塊,來顯示如果做了插值的樣子(我從 Rails 程式碼學來的一個實踐):

# 從 activesupport/lib/active_support/core_ext/string/output_safety.rb
UNSAFE_STRING_METHODS.each do |unsafe_method|
  if 'String'.respond_to?(unsafe_method)
    class_eval <<-EOT, __FILE__, __LINE__ + 1
      def #{unsafe_method}(*args, &block)       # def capitalize(*args, &block)
        to_str.#{unsafe_method}(*args, &block)  #   to_str.capitalize(*args, &block)
      end                                       # end

      def #{unsafe_method}!(*args)              # def capitalize!(*args)
        @dirty = true                           #   @dirty = true
        super                                   #   super
      end                                       # end
    EOT
  end
end

元程式設計避免使用 method_missing。會讓 Backtraces 變得很凌亂;行為沒有列在 #methods 裡;拼錯的方法呼叫可能默默的工作(nukes.luanch_state = false)。考慮使用 delegation, proxy, 或是 define_method 來取代。如果你必須使用 method_missing。 確保也定義了 respond_to?

僅捕捉字首定義良好的方法,像是 find_by_* ― 讓你的程式碼愈肯定(assertive)愈好。

在最後的敘述句(statement)呼叫 super

從 delegate 到 assertive, 不神奇的(non-magical)方法:

# 不好
def method_missing?(meth, *args, &block)
  if /^find_by_(?<prop>.*)/ =~ meth
    # ... lots of code to do a find_by
  else
    super
  end
end

# 好
def method_missing?(meth, *args, &block)
  if /^find_by_(?<prop>.*)/ =~ meth
    find_by(prop, *args, &block)
  else
    super
  end
end

# 而最好是在每個可找到的屬性被宣告時,使用 define_method 。

其它

ruby -w 寫出安全的程式碼。

避免使用雜湊作為選擇性參數。這個方法是不是做太多事情了?

避免方法長於 10 行程式碼(LOC)。理想上,大部分的方法會小於 5 行。空行不算進 LOC 裡。

避免參數列表長於三或四個參數。

如果你真的需要,加入“全域”變數到核心以及把它們設為私有的。

使用實體變數而不是全域變數。

# 不好
$foo_bar = 1

# 好
class Foo
  class << self
    attr_accessor :bar
  end
end

Foo.bar = 1

alias_method 可以做到時,避免使用 alias

使用 OptionParser 來解析複雜的命令行選項及 ruby -s 來處理瑣碎的命令行選項。

用函數式的方法寫程式,在有意義的情況下避免賦值。

避免不需要的元程式設計。

不要變動參數,除非那是方法的目的。

避免超過三行的巢狀區塊。

保持一致性。在理想的世界裡,遵循這些準則。 使用常識。

貢獻

在本指南所寫的每個東西都不是定案。這只是我渴望想與同樣對 Ruby 程式設計風格有興趣的大家一起工作,以致於最終我們可以替整個 Ruby 社群創造一個有益的資源。

歡迎開票或發送一個帶有改進的更新請求。在此提前感謝你的幫助!

口耳相傳

一份社群策動的風格指南,對一個社群來說,只是讓人知道有這個社群。推特這個指南,分享給你的朋友或同事。我們得到的每個註解、建議或意見都可以讓這份指南變得更好一點。而我們想要擁有的是最好的指南,不是嗎?

Rails 風格指南

序幕

這份指南目的於演示一整套 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 )

創立一個與生產環境(production enviroment)相似的額外 staging 環境。

路由

當你需要加入一個或多個動作至一個 RESTful 資源時(你真的需要嗎?),使用 member and collection 路由。

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

控制器

讓你的控制器保持苗條 ― 它們應該只替視圖層取出資料且不包含任何業務邏輯(所有業務邏輯應當放在模型裡)。

每個控制器的行動應當(理想上)只調用一個除了初始的 findnew 方法。

控制器與視圖之間共享不超過兩個實體變數(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-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4} $/i
  validates_length_of :content, :maximum => 500
end

更完整的範例,參考 [RailsCast on the subject](http://railscasts.com/episodes/326-activeattr)。

ActiveRecord

避免改動預設的 ActiveRecord(表的名字、主鍵,等等),除非你有一個非常好的理由(像是不受你控制的資料庫)。

把巨集風格的方法放在類別定義的前面(has_many, validates, 等等)。

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

class Person
  validates :email, email: true
end

所有慣用的驗證器應放在一個共享的 gem 。

自由地使用命名的作用域(scope)。

當一個由 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

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 方法,並移除擴展的部份。

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 方法取代 updown 方法。

# 過去的方式
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]+\.)+[az]{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,ym​​l}')]

把共享的本土化選項,像是日期或​​貨幣格式,放在 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, 或圖片。

第三方程式碼如: jQuerybootstrap 應放置在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: 'smt​​p.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-rails3roadie

應避免頁面產生響應時寄送 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 應用的過程,像是 Rails、Sinatra 或 Merb。 Capybara 模擬了真實用戶使用 web 應用的互動。它與你測試在運行的驅動無關,並原生搭載 Rack::Test 及 Selenium 支持。透過外部 gem 支持 HtmlUnit、WebKit 及 env.js 。與 RSpec & Cucumber 一起使用時工作良好。

carrierwave - Rails 最後一個文件上傳解決方案。支持上傳檔案(及很多其它的酷玩意的)的本機儲存與雲端儲存。圖片後處理與 ImageMagick 整合得非常好。

clientsidevalidations - 一個美妙的 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 整合。

simpleform - 一旦用過 simpleform(或 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"
# ...

# 步驟:
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

大量使用 descibecontext

如下地替 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 一個方法
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 方法沒有傳入參數時,是這麼使用的。

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

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
  let(:article) { Fabricate(:article) }
end

加入一個例子確保捏造的模型是可行的。

describe Article
  it 'is valid with valid attributes' do
    article.should be_valid
  end
end

當測試驗證時,使用 have(x).errors_on 來指定要被驗證的屬性。使用 be_valid 不保證問題在目的的屬性。

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

替每個有驗證的屬性加另一個 describe

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

當測試模型屬性的獨立性時,把其它物件命名為 another_object

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

Mailers

在 Mailer 測試的模型應該要被 mock。 Mailer 不應依賴建模。

Mailer 的測試應該確認如下:

這個 subject 是正確的

這個 receiver e-mail 是正確的

這個 e-mail 寄送至對的郵件地址

這個 e-mail 包含了需要的訊息

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

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 社群創造一個有益的資源。

歡迎開票或發送一個帶有改進的更新請求。在此提前感謝你的幫助!

口耳相傳

一份社群驅動的風格指南,對一個社群來說,只是讓人知道有這個社群。推特轉發這份指南,分享給你的朋友或同事。我們得到的每個註解、建議或意見都可以讓這份指南變得更好一點。而我們想要擁有的是最好的指南,不是嗎?