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
使用方法pending
和skip
區別
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
裡可以有多個 it
,it
裡面也可以有多個期望語句哦,上例可改寫為:
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
方法
每條測試就是一個 it
,it
要放在 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
另有兩個同義字 specify
和 example
。少有人用,除非團隊風格因素,否則請避免使用。
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 http://rspec.info/documentation/3.5/rspec-core
- rspec-mocks http://rspec.info/documentation/3.5/rspec-mocks
- rspec-expectations http://rspec.info/documentation/3.5/rspec-expectations
- rspec-rails http://rspec.info/documentation/3.5/rspec-rails
輸入類別名稱搜尋:
輸入方法名稱搜尋: