RSpec 介紹

RSpec 是一個用 Ruby 寫的測試框架。是目前 Ruby 社群最流行的測試工具之一,另外一個是 Minitest,Minitest 就是純 Ruby,而 RSpec 提供了一套可讀性較強的 DSL。

讓我們看看 RSpec 的測試長怎麼樣:

describe Juanito do
  it "Born to be hungry" do
    juanito = Juanito.new

    expect(juanito.hungry?).to be true
  end

  it "Eat beats hunger" do
    juanito = Juanito.new
    juanito.eat(something)

    expect(juanito.hungry?).to be false
  end
end

怎麼樣,讀起來有沒有像英文呢?用 Minitest 寫起來就會像是:

class Juanito < Minitest::Test
  def test_born_to_be_hungry
    juanito = Juanito.new

    assert juanito.hungry?
  end

  def test_eat_beats_hunger
    juanito = Juanito.new
    juanito.eat something

    refute juanito.hungry?
  end
end

各有各自的擁護者,本書是使用 RSpec 介紹如何撰寫 Rails 的測試,當然會了以後,要把 RSpec 應用到任何 Ruby 的專案都可以,不僅可以用在 Rails 項目。

RSpec 基礎

  • describe / it / expect 角色區別
  • context 使用方法
  • before 使用方法
  • let / let! / subject 使用方法
  • pendingskip 區別

describe 可以描述要測的行為(以字串表示,比如下例的四則運算):

describe "Arithmetic" do
  it "1 + 1 equals 2" do
    expect(1 + 1).to eq 2
  end
end

但四則運算不只有加法,還有減法:

describe "Arithmetic" do
  it "1 + 1 equals 2" do
    expect(1 + 1).to eq 2
  end

  it "1 - 1 equals 0" do
    expect(1 - 1).to eq 0
  end
end

乘法、除法:

describe "Arithmetic" do
  it "1 + 1 equals 2" do
    expect(1 + 1).to eq 2
  end

  it "1 - 1 equals 0" do
    expect(1 - 1).to eq 0
  end

  it "1 * 1 equals 1" do
    expect(1 * 1).to eq 1
  end

  it "1 / 1 equals 1" do
    expect(1 / 1).to eq 1
  end
end

從上例可看到,describe “描述”一組測試,用 it 來寫一條測試。it 內的 expect 則是用來檢查“左值”是否與“右值”相等:

expect(“左值”).to eq(“右值”) # eq 的括號可省略

所以 describe 裡可以有多個 itit 裡面也可以有多個期望語句哦,上例可改寫為:

describe "Arithmetic" do
  it "+-*/" do
    expect(1 + 1).to eq 2
    expect(1 - 1).to eq 0
    expect(1 * 1).to eq 1
    expect(1 / 1).to eq 1
  end
end

每個測試檔案都是由 RSpec.describe 描述要測試的類別 Class、模組 Module:

RSpec.describe User do
  ...
end

RSpec.describe ApplicationHelper do
  ...
end

describe 可以嵌套使用,比如用來描述類別內不同的方法:

describe User do
  describe ".find_or_create_with" do

  end

  describe ".acitve" do

  end

  describe "#github_url" do

  end

  describe "#token" do

  end
end

對應的類別:

class User
  def self.find_or_create_with(info)
    ...
  end

  scope :active, -> { ... }

  def github_url
    ...
  end

  def token
    ...
  end
end

RSpec.describe 後區塊裡的內容則是我們的測試內容。

RSpec.describe User do
  ...
end

測試可以用 describe 來組織,比如測試類別方法、實體方法:

RSpec.describe User do
  describe ".find_or_create_with" do

  end

  describe "#token" do

  end
end

describe 裡使用 it 來撰寫測試“內容”:

RSpec.describe User do
  describe "#token" do
    it "returns original value of token on call" do
      user = build(:user)
      user.token = "original-token"

      user.save

      expect(user.reload.token).to eq("original-token")
    end
  end
end

每個方法內有 if-else,不同的情況,可以用 context 來區分。

RSpec.describe User do
  describe ".find_or_create_with" do
    context "with existing user" do
      ...
    end

    context "with new user" do
      ...
    end
  end
end

it 方法

每條測試就是一個 itit 要放在 describe 區塊裡面。

RSpec.describe "It" do
  it "todo" # pending

  it { # "a executable test without description"

  }

  it "executable test with description" do
  end
end

這個 it 第一個參數是測試的名字(名字會在運行 $ rspec --format documentation 時印出來),第二個參數是 Ruby 的區塊(block),區塊就是測試要測的事情。

Ruby 區塊單行偏好用 { ... },多行用 do ...; end。參考 Ruby 風格指南。建議一致使用 do...; end

it 有三種寫法:

  • 只有名字,沒區塊。代表待測事項。上例的 todo (PENDING: Not yet implemented)

    it "some descriptive text"
    

    等同於

    pending "some descriptive text"
    
  • 沒有名字,有單行區塊。用來寫一行可以搞定的測試。

    it { ... }
    
  • 沒有名字,有多行區塊。用來寫需要多行的測試。

    it "some descriptive text" do
      ...
    end
    

執行這個測試的結果會像是這樣:

It
  todo (PENDING: Not yet implemented)
  example at ./spec/it_spec.rb:4
  executable test with description

Pending: (Failures listed here are expected and do not affect your suite's status)

  1) It todo
     # Not yet implemented
     # ./spec/it_spec.rb:2


Finished in 0.00048 seconds (files took 0.10223 seconds to load)
3 examples, 0 failures, 1 pending

it 另有兩個同義字 specifyexample。少有人用,除非團隊風格因素,否則請避免使用。

describe 方法

describe 用來把測試(it)分組,譬如測試 String 類別下的實體方法(instance method): capitalize 以及 size,就可以這樣寫:

RSpec.describe String do
  describe "#capitalize" do
    # ...
  end

  describe "#size" do
    # ...
  end
end

這裡有一個 Ruby 的命名慣例:

  • 類別方法的測試,使用一個點(.),加上方法名稱: .method_name 來引用。
  • 實體方法的測試,使用一個井號(#),加上方法名稱: #method_name 來引用。
# foo/ruby.rb
class Ruby
  def initialize(programmer)
  end

  def self.goal
    "make programmer happy"
  end

  def happy?
    true
  end
end

# foo/programmer.rb
class Programmer
end

# foo/ruby_spec.rb
require_relative "./ruby"
require_relative "./programmer"

RSpec.describe Ruby do
  describe ".goal" do
    it { expect(Ruby.goal).to eq "make programmer happy" }
  end

  describe "#happy?" do
    it "yes, always happy" do
      programmer = Programmer.new

      rubyist = Ruby.new(programmer)

      expect(rubyist.happy?).to eq true
    end
  end
end
$ rspec ruby_spec.rb

Ruby
  .goal
    should eq "make programmer happy"
  #happy?
    hell yes, always happy

Finished in 0.00092 seconds (files took 0.10544 seconds to load)
2 examples, 0 failures

context 方法

而需要考慮多種情況則使用 RSpec 提供的 context (是 describe 的 alias 同義字,但為了可讀性,所以另外新增了這個語法):

context "when credit card is updated" do
  ...
end

context "when credit card fails to update" do
  ...
end

context 通常用來表示不同的狀況,譬如“當”使用者是管理員,“使用者有某某某屬性”等。常用的英文有 when, with。

Matcher

測試就是檢查結果(實際的數值),跟執行的結果是否符合預期。RSpec 的語法:

expect(actual_value).to eq(expected_value)

而這裡的 eq 就是 Matcher。

RSpec 提供很多 Matchers 可以使用,這裡建議只要先學會 eq 即可,一切的 Matcher 都可以從 eq Matcher 變化出來,使用了對的 Matcher 的好處是錯誤訊息可讀性更高。

舉個例子,要檢查一個 Array 裡是否有 3 這個數值可以用 RSpec 的 include matcher 寫:

expect(array).to include(3)

但也可以寫成

expect(array.include?(3)).to eq true

RSpec 所有的 Matcher 可以在 rspec-expectations 這個 Gem 找到。

最基本的 Matchers

  • 檢查相等

    Syntax:

    expect(expression_1).to eq(expression_2)
    

    Example:

    expect(1 + 1).to eq(2)
    
  • 檢查是否拋出錯誤

    Syntax:

    expect { }.to raise_error(ErrorClassName)
    

    Example:

    expect { obj.some_method_not_exists }.to raise_error(NoMethodError)
    
  • 檢查狀態變更

    expect { User.create! }.to change(User.count).by(1)
    

這裡可能對於為什麼有時候要用:

expect(....)

有時候要用:

expect { ... }

感到疑惑。

數值用括號,一段代碼用區塊 { ... }

只要學會這三個基本上可以應付 90% 的情況。

所有的 Matcher 見:https://www.relishapp.com/rspec/rspec-expectations/docs/built-in-matchers

RSpec 文件

使用任何東西之前,都要先知道可以去那裡找文件。

官方文件導覽

去這裡可以 http://rspec.info/documentation 找到 API 和 Relish 文件

Relish 文件

https://www.relishapp.com/rspec

這裡文件提供“可執行的範例”,各種用法,特色等。適合了解 RSpec 可以做什麼,怎麼用、怎麼學等,詳細用法還是得查閱 API 底層文件。Relish 上的範例都是以 Cucumber 寫成,與當下 RSpec 的原始碼同步。

API 底層文件

詳述了所有公開的 API:

RSpec Core API 頁面

輸入類別名稱搜尋:

類別導覽,可模糊搜尋

輸入方法名稱搜尋:

方法導覽,可模糊搜尋

去那裡發問

results matching ""

    No results matching ""