<% content_for :menu do %>
  • rspec 入門指南
  • » 如何描述 (describe) 你的 methods
  • » 使用 context
  • » 保持簡潔的 description
  • » 測試單一條件
  • » 驗證所有可能的情況
  • » 善用 subject
  • » 善用 let 和 let!
  • » Mock 的時機
  • » 只建立必要的資料
  • » 取 factory 捨 fixture
  • » 一目瞭然的 matcher
  • » 通用測項
  • » 測你所見
  • » Description 不提 should
  • » 用 guard 自動化測試
  • » 用 spork 縮短測試時間
  • » 偽裝 HTTP request
  • » 好用的 formatter
  • Books
  • Presentations
  • Resources on the web
  • Screencasts
  • Libraries
  • Styleguide
  • Improving Better Specs
  • Credits
  • Help us
  • <% end %>

    RSpec 是一個好工具。它在 BDD 開發流程中被用來撰寫可讀性高的規格(測項),引導並驗證你所開發的應用程式。

    網路上多半的資源告訴你 RSpec 能「做些什麼」,但很少討論如何使用它「做出好的規格(測項)」。

    Better Specs 盡可能地收集開發者們經年累月習得的 "Best practice" 來幫助你達到這個目標。

    如何描述 (describe) 你的 methods

    清楚地描述你的 method。譬如在提到 class method 時加上 Ruby 文件慣用的 . (或 ::),在提到 instance method 時加上 #

    bad

    describe 'the authenticate method for User' do
    describe 'if the user is an admin' do
    

    good

    describe '.authenticate' do
    describe '#admin?' do
    

    進一步討論由此去 →

    使用 context

    Context 讓你的測項更明確、有條理,在漫長的開發過程中保持可讀性。

    bad

    it 'has 200 status code if logged in' do
      response.should respond_with 200
    end
    it 'has 401 status code if not logged in' do
      response.should respond_with 401
    end
    

    good

    context 'when logged in' do
      it { is_expected.to respond_with 200 }
    end
    context 'when logged out' do
      it { is_expected.to respond_with 401 }
    end
    

    描述 context 時,要用 "when" 或 "with" 做開頭。

    進一步討論由此去 →

    保持簡潔的 description

    控制 spec 的描述字數在 40 字以內,超過的話應該用 context 分段。

    bad

    it 'has 422 status code if an unexpected params will be added' do
    

    good

    context 'when not valid' do
      it { should respond_with 422 }
    end
    

    上例中我們把 status code 相關的描述用測項本體 it { should respond_with 422 } 取代。 如果你用 rspec filename 執行這個測項,仍然會輸出具可讀性的報告。

    Formatted Output

    when not valid
      it should respond with 422
    

    進一步討論由此去 →

    測試單一條件

    「單一條件」意指一個測項應該只帶有一個檢查 (expection, assertion)。這樣做能幫助你直接前往失敗的測項尋找可能的問題,也讓程式碼比較好看。

    獨立的規格單元測項中,每一題應該只定義「一個行為」。用上多個檢查表示你可能在一題裡定義了多個行為。

    但在涉及 DB 、外部 webservice、或是整合測試這類非獨立的測項裡,不斷做重覆的前置設定 (setup) 會拖慢測試效率,倒不如在一個測項放上多個檢查。我認為這種跑不快的測項可以一題檢查一個以上的行為。

    good (isolated)

    it { should respond_with_content_type(:json) }
    it { should assign_to(:resource) }
    

    Good (not isolated)

    it 'creates a resource' do
      response.should respond_with_content_type(:json)
      response.should assign_to(:resource)
    end
    
    進一步討論由此去 →

    驗證所有可能的情況

    實行測試很好,但是如果測項沒有包括 edge case 的話,它並不能發揮最大的效用。有效的、無效的、和 edge case 都需要被驗證,可以參考下述範例的作法。

    Destroy action

    before_action :find_owned_resources
    before_action :find_resource
    
    def destroy
      render 'show'
      @consumption.destroy
    end

    我常在只針對是否成功移除 resource 的測項裡看到失誤。這類行為至少還包括兩個 edge case:要移除的 resource 不存在,以及無權限移除。切記,考慮所有可能的輸入值並對它們進行測試。

    bad

    it 'shows the resource'
    

    good

    describe '#destroy' do
    
      context 'when resource is found' do
        it 'responds with 200'
        it 'shows the resource'
      end
    
      context 'when resource is not found' do
        it 'responds with 404'
      end
    
      context 'when resource is not owned' do
        it 'responds with 404'
      end
    end
    

    進一步討論由此去 →

    善用 subject

    當多個測項針對的 subject 相同時,善用 subject{} 取代重覆的程式碼 (DRY)。

    bad

    it { assigns('message').should match /it was born in Belville/ }
    

    good

    subject { assigns('message') }
    it { should match /it was born in Billville/ }
    

    RSpec 同時提供命名 subject 的功能。

    Good

    subject(:hero) { Hero.first }
    it "carries a sword" do
      hero.equipment.should include "sword"
    end
    

    請看更多關於 rspec subject 的資訊。

    進一步討論由此去 →

    善用 let 和 let!

    當你需要指定 variable 時,用 let 代替 before 來建立 instance variable。 let 有 lazy load 特性,只在測項第一次用到該 variable 時被執行,並且會 cache 直到該測項結束。 想更深入瞭解 let 請參考這個 stackoverflow answer

    bad

    describe '#type_id' do
      before { @resource = FactoryBot.create :device }
      before { @type     = Type.find @resource.type_id }
    
      it 'sets the type_id field' do
        @resource.type_id.should equal(@type.id)
      end
    end
    

    good

    describe '#type_id' do
      let(:resource) { FactoryBot.create :device }
      let(:type)     { Type.find resource.type_id }
    
      it 'sets the type_id field' do
        resource.type_id.should equal(type.id)
      end
    end
    

    let 進行的初始化會在測項執行時以 lazy load 方式完成。

    good

    context 'when updates a not existing property value' do
      let(:properties) { { id: Settings.resource_id, value: 'on' } }
    
      def update
        resource.properties = properties
      end
    
      it 'raises a not found error' do
        expect { update }.to raise_error Mongoid::Errors::DocumentNotFound
      end
    end
    

    如果想要 variable 在定義時就被建立,請用 let!。這個技巧在產生 database 內容以測試 query 和 scope 時十分好用。

    以下是一個 let 的實例。

    good

    # this:
    let(:foo) { Foo.new }
    
    # is very nearly equivalent to this:
    def foo
      @foo ||= Foo.new
    end
    

    請看更多關於 rspec let 的資訊。

    進一步討論由此去 →

    Mock 的時機

    關於 mock 的用法仍有爭議。能對真實行為測試的時候,不要(過度)依賴 mock。真實的測項在你改善應用程式流程時十分有幫助。

    good

    # simulate a not found resource
    context "when not found" do
      before { allow(Resource).to receive(:where).with(created_from: params[:id]).and_return(false) }
      it { should respond_with 404 }
    end
    

    Mock 能改善測項的執行速度,但它並不容易上手。你必須對 mock 更熟悉才能讓它正確地派上用場,請看更多的說明

    進一步討論由此去 →

    只建立必要的資料

    如果你有參與過中型的專案 (有些小專案也如此),跑測試可能是件快不起來的工作。為了解決這個問題,千萬不要載入非必要的資料。如果你發現你需要上打的 record,你可能用錯方法了。

    good

    describe "User"
      describe ".top" do
        before { FactoryBot.create_list(:user, 3) }
        it { User.top(2).should have(2).item }
      end
    end
    

    進一步討論由此去 →

    取 factory 捨 fixture

    這是個值得重彈的老調。不要用 fixture,它太難維護了。改用 fatory,它能減輕建立新資料負擔。

    bad

    user = User.create(
      name: 'Genoveffa',
      surname: 'Piccolina',
      city: 'Billyville',
      birth: '17 Agoust 1982',
      active: true
    )
    

    good

    user = FactoryBot.create :user
    

    另外請看這篇文章。當討論到 unit test 的時候,最佳情況是不用 fixture 也不用 factory。盡可能把你的 domain logic 留在那些不用靠 factory 和 fixture 進行複雜耗時前置設定的函式庫裡。

    請看更多關於 Factory Bot 的資訊。

    進一步討論由此去 →

    一目瞭然的 matcher

    善用 rspec 內建 或意義簡明的 matcher 。

    bad

    lambda { model.save! }.should raise_error Mongoid::Errors::DocumentNotFound
    

    good

    expect { model.save! }.to raise_error Mongoid::Errors::DocumentNotFound
    

    進一步討論由此去 →

    通用測項

    撰寫測項是個好習慣,能增加你開發過程中的信心。但漸漸你會發現裡頭出現越來越多重覆的程式碼,你需要通用測項讓你的測試更 DRY。

    bad

    describe 'GET /devices' do
      let!(:resource) { FactoryBot.create :device, created_from: user.id }
      let(:uri) { '/devices' }
    
      context 'when shows all resources' do
        let!(:not_owned) { FactoryBot.create factory }
    
        it 'shows all owned resources' do
          page.driver.get uri
          page.status_code.should be(200)
          contains_owned_resource resource
          does_not_contain_resource not_owned
        end
      end
    
      describe '?start=:uri' do
        it 'shows the next page' do
          page.driver.get uri, start: resource.uri
          page.status_code.should be(200)
          contains_resource resources.first
          page.should_not have_content resource.id.to_s
        end
      end
    end
    

    good

    describe 'GET /devices' do
    
      let!(:resource) { FactoryBot.create :device, created_from: user.id }
      let(:uri)       { '/devices' }
    
      it_behaves_like 'a listable resource'
      it_behaves_like 'a paginable resource'
      it_behaves_like 'a searchable resource'
      it_behaves_like 'a filterable list'
    end
    

    經驗上來看,通用測項主要用在 controller 上。因為不同 model 間差異較大,少有通用的邏輯。

    請看更多關於 rspec shared examples 的資訊。

    進一步討論由此去 →

    測你所見

    詳盡地檢驗 model 和應用程式的整合行為,不要浪費複雜卻無用的測試在 controller 上。

    一開始測試 app 時,我花了精力在 controller 上,現在我不那麼做了。取而代之我只用 RSpec 和 Capybara 建立一些整合測項。 我的想法是你應該測試會被看見的東西,而對 controller 來說測試是多餘的。你會發現大部分的測項與 model 息息相關,同時整合性的測項很容易整理成通用測項,讓你的測試簡明易懂。

    這個具爭議性的想法在 Ruby 社群中仍未定論,正反雙方都有好理由支持各自的論點。認為 controller 也需要測試的人會告訴你整合測試跑不快,而且無法窮舉所有情況。

    他們錯了。你可以輕易測到所有可能,而且利用 Guard 這類自動化測試工具只執行單一檔案的測項。如此一來只會跑到需要驗證的測項,費時很短,不會擔誤你的 flow。

    進一步討論由此去 →

    Description 不提 should

    別在測項描述提到 should,要用第三人稱的現在式。更進一步,你可以開始試用新的 expectation 語法。

    bad

    it 'should not change timings' do
      consumption.occur_at.should == valid.occur_at
    end
    

    good

    it 'does not change timings' do
      expect(consumption.occur_at).to equal(valid.occur_at)
    end
    

    請問 should_notshould_clean 這兩個 gem,他們教你如何在 RSpec 實踐上述原則以及清理手上那些用 "should" 開頭的測項。

    進一步討論由此去 →

    用 guard 自動化測試

    一對程式做了修改就得跑過所有測項可能會成為負擔,這會消秏許多時間而且打斷你的 flow。Guard 可以基於你正在修改的測項本身、model、controller 或是檔案,從完整的測試裡只挑出相關的測項執行。

    good

    bundle exec guard
    
    以下 Guardfile 範例提供一些基本的載入規則。

    good

    guard 'rspec', cli: '--drb --format Fuubar --color', version: 2 do
      # 執行所有被修改的 spec
      watch(%r{^spec/.+_spec\.rb$})
      # 執行 lib 裡被修改的 file 對應的 lib spec
      watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
      # 執行被修改的 model 對應的 model spec
      watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
      # 執行被修改的 view 對應的 view spec
      watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
      # 執行與改動的 controller 相關的 integration spec
      watch(%r{^app/controllers/(.+)\.rb}) { |m| "spec/requests/#{m[1]}_spec.rb" }
      # 當 application controller 改動時執行所有的 integration test
      watch('app/controllers/application_controller.rb') { "spec/requests" }
    end
    

    Guard 好用但不能滿足你所有的需求。有時設一組快速鍵在你想測的時候執行你需要的測項,與你的 TDD 流程更合的來。然後你可以利用 rake task 在 push code 之前跑過完整的測試。這裡有些 給 vim 用的快速鍵設定

    請看更多關於 guard-rspec 的資訊。

    進一步討論由此去 →

    用 spork 縮短測試時間

    測試 Rails 時會載入整個 Rails app,這滿秏時並且可能打斷你開發的 flow。解決方法是利用 ZeusSpin、或 Spork 這類的工具。 它們會預先載入所有你通常不會改到的函式庫,然後再重新載入那些你經常改動的 controller、model、view、factory 等檔案。

    這裡提供你基於 Spork 設置的 spec helperGuardfile。這個設定會在預先載入的檔案 (像是 initializer) 被改到時重新載入整個 app,執行單一測項的速度會非常非常地快。

    Spork 的缺點在它過分地 monkey-patch 了你的程式碼,你可能花上半天試著搞懂為什麼沒有重新載入某個檔案。 如果你有使用 Spin 或其他解決方案的例子,請 與我們分享

    這是使用 Zeus 的 Guardfile 設定。spec_helper 的部分不需要修改,但你必須在開始測試前開一個 console 執行 `zeus start`。

    雖然 Zeus 採取不像 Spork 那麼激進的作法,它最大的問題在使用上有嚴格的要求:要求 Ruby 1.9.3+ (建議使用 Ruby 2.0 的 backported GC) 及支持 FSEvents 或 inotify 的作業系統。

    很多對 Spork 不滿的人改用了其他的解決方案。但比起這些補救性質的工具,更好的作法是從設計上改進,以方便直接抓出那些相依的檔案。 請進一步參考下面討論連結裡的內容。

    進一步討論由此去 →

    偽裝 HTTP request

    有時你會存取外部的服務,但沒辦法真的使用這些服務來測試。這時候就需要 webmock 這類工具來進行 stub。

    good

    context "with unauthorized access" do
      let(:uri) { 'http://api.lelylan.com/types' }
      before    { stub_request(:get, uri).to_return(status: 401, body: fixture('401.json')) }
      it "gets a not authorized notification" do
        page.driver.get uri
        page.should have_content 'Access denied'
      end
    end
    

    請看更多關於 webmock 的資訊和 影片。 另外有個不錯的 presentation 說明如何交互利用這些工具。

    進一步討論由此去 →

    好用的 formatter

    使用 formatter 讓你的測試提供有用的訊息。我推薦 fuubar,只要安裝這個 gem 並在 Guardfile 裡設定 fuubar 為預設的 formatter 就可以用了。

    good

    # Gemfile
    group :development, :test do
      gem 'fuubar'
    

    good

    # Guardfile
    guard 'rspec' do
      # ...
    end
    

    good

    # .rspec
    --drb
    --format Fuubar
    --color
    

    Learn more about fuubar.

    進一步討論由此去 →

    Books

    <%= render "partials/books" %>

    Presentations

    RSpec 2 Best practices from Andrea Reginato

    Resources on the web

    1. Everyday Rails Spec
    2. Eggs on Bread Best Practices
    3. The Carbon Emitter Best Practices
    4. Andy Vanasse Best Practices
    5. Bitfluxx Best Practices
    6. Dmytro Shteflyuk Best Practices
    7. thoughtbot's RSpec Related Reading

    Screencasts

    1. RSpec on PeepCode
    2. Testing With RSpec Code School course
    3. Spork Railscast
    4. Using Zeus to speed up your tests
    5. Code TV Screencast on Guard and Spork
    6. Many of Destroy All Software screencasts

    Libraries (documentation)

    1. RSpec Documentation
    2. Capybara Documentation
    3. Factory Bot Documentation
    4. Webmock Documentation
    5. Timecop Documentation
    6. Shoulda Matchers
    7. Fuubar Release

    Styleguide

    We are seeking for the best guidelines to write "nice to read" specs. Right now a good starting point is for sure the Mongoid test suite. It uses a clean style and easy-to-read specs, following most of the guidelines described here.

    Improving Better Specs

    This is an open source project. If something is missing or incorrect just file an issue to discuss the topic. Also check the following issues:

    Credits

    The document was started by Andrea Reginato. A special thanks to the Lelylan Team. This document is licensed under MIT License.

    Help us

    If you have found these tips useful and they improve your work, think about making a $9 donation. Any donations will be used to make this site a more compleate reference for better testing in Ruby.