和 double 的差別
首先就是 Spies
本身更狡詐了一些,還記得在寫 double
或是 instance double
的時候,常常要在先去期望這個變數獲得什麼樣的方法,並寫在它的後面。
像是這樣:
RSpec.describe 'double' do
let(:store) { double('store', sell: 'earn the money!')}
it 'invoke method first' do
expect(store.sell).to eq('earn the money!')
end
end
但 Spies
本身是可以先寫方法出來的,就像正常的測試一樣,只是 expectation
的部分需要做一些改造!
像是這樣:
RSpec.describe 'spies' do
let(:burger) { spy('burger') }
it 'confirm a message has been received' do
burger.eat
expect(burger).to have_received(:eat)
end
end
其實有趣的地方在於,這邊的 expectation
是 have_received
,有種在玩英文文法的感覺,證明這個漢堡已經接收到 eat
方法!
而 Spies
也有它的檢查機制存在,若是我們沒有收到的方法,但卻有的話會發生什麼事呢?
RSpec.describe 'spies' do
let(:burger) { spy('burger') }
it 'confirm a message has been received' do
burger.eat
expect(burger).to have_received(:eat)
expect(burger).to have_received(:throw)
end
end
這個變數期待著一次的 throw
方法,但卻沒有,所以 RSpec 顯示錯誤。
那至於這個和 instance double
之間要如何做選擇呢?這邊先賣個關子,後面會有有趣的實驗來看看差別在哪裡。
使用情境
其實使用的情境和 instance double
基本上是很像的,都是為了避免依賴程式碼的干擾,讓測試的獨立性更好,更專注在要測試的部分。
而不是為了生出別的實體變數而搞得滿目瘡痍。
這邊再用一段簡單的程式碼來敘述一下情境:
class Cheese
def initialize(type)
@type = type
end
end
class Burger
attr_reader :cheese
def initialize
@cheese = []
end
def add(type)
@cheese << Cheese.new(type)
end
end
這邊很清晰的是我們要在漢堡裡面加入 cheese
,就像前幾天談到的一樣,要專注在 Burger
這個類別,所以我們希望可以把 第 15 行的 Cheese.new(type)
這段給獨立出來,用 spy 的方式替代!
接著我們來寫會通過的測試!
RSpec.describe Burger do
let(:cheese) { spy(Cheese) }
before do
allow(Cheese).to receive(:new).and_return(cheese)
end
describe '#add' do
it 'add cheese to burger' do
subject.add('parmesan')
expect(Cheese).to have_received(:new).with('parmesan')
expect(subject.cheese.length).to eq 1
expect(subject.cheese.first).to eq(cheese)
end
end
end
逐行來解釋一下,首先在進入測試之前 ( 第五行 ),我們先做了允許 Cheese
這個類別接收 new
方法,並且 return 我們的 spy(Cheese)
接著進入測試的 block 中,subject
執行了 add
方法,並且傳入參數 parmesan。
接著又說這個 Cheese
已經接受了 new
方法,也就是我們在 before
block 中所做的事情,在這個時候,我們已經徹底的替換掉正式的程式碼了。
接著的 expectation
就是基本的操作囉!
但你會發現,這個 spy
替換成 instance_double
也能夠正常的使用。
奇怪了… 那我們印出來看看吧!我們先印印看 spy
版本的
RSpec.describe Burger do
let(:cheese) { spy(Cheese) }
before do
allow(Cheese).to receive(:new).and_return(cheese)
end
describe '#add' do
it 'add cheese to burger' do
subject.add('parmesan')
expect(Cheese).to have_received(:new).with('parmesan')
p cheese # 印出來看看吧
expect(subject.cheese.length).to eq 1
expect(subject.cheese.first).to eq(cheese)
end
end
end
有沒有搞錯?這個 spies
講得這麼厲害,但結果是 double
的語法糖衣嗎?
快印看看 instance double
是什麼:
RSpec.describe Burger do
let(:cheese) { instance_double(Cheese) }
before do
allow(Cheese).to receive(:new).and_return(cheese)
end
describe '#add' do
it 'add cheese to burger' do
subject.add('parmesan')
expect(Cheese).to have_received(:new).with('parmesan')
p cheese # 印出來看看吧
expect(subject.cheese.length).to eq 1
expect(subject.cheese.first).to eq(cheese)
end
end
end
結果最後還是 instance double
,以後都用他就好了…
但最終要如何使用,還是取決於功能的考量,和團隊的取向,只要能夠好好的測試到需要測試的功能就好了!
所以你要說 spies
和 double
有真的一模一樣嗎?也沒有啦,其實,spies
再做得事情更像是潛入敵營,偽裝成某一段程式碼的感覺,而且 have_received
也真的需要接受方法,只是需要回傳值的話還是得使用 allow
。
還是有很大的不同在於撰寫的模式,閱讀起來的舒適度!
結語
明天開始會正式進入 rspec-rails
的章節!
也是 RSpec 真正的戰場,剛好加上最近上班也都在寫的緣故,希望可以把一些實際的案例放入文章中,讓需要的人也能夠看到!
基本的 Model 測試, Feature 測試以及如何 Stub API 的方式都會盡量地寫進去。