昨天提過的 allow method
,在昨天的範例裡面就有使用到。
使用的時機在於允許 double
接受方法以及回傳值。
RSpec.describe 'allow method review' do
it "can customize return value for methods on doubles" do
calculator = double
allow(calculator).to receive(:add).and_return(15)
expect(calculator.add).to eq(15)
expect(calculator.add(3)).to eq(15)
expect(calculator.add(132353123)).to eq(15)
end
end
可以看到一個 calculator
被允許接受一個 add
方法,然後回傳值為 15。
接著要來說說昨天提到的 double method
有什麼壞處。
首先,整個 double
物件都是寫死的,這可能會在開發的測試上造成一些不必要的壓力,因為我們必須時時刻刻注意這個 double
物件的方法有沒有更動。
若是我們不慎移除掉真實的 Code,他依舊會通過測試,因為我們的測試裡面還是保有這個方法,因為它不是真實的,一切都是捏造的!
還記得一開始用 double
的想法就是希望我們可以拋開其他依賴的干擾,專注在我們要測試的方法上面,但這有可能害我們錯失找到 Bug 的關鍵!
所以我們需要更嚴謹一些,於是就有了 Instance Doubles
。
Instance Doubles
所以說,為什麼要嚴謹一些呢?
class Burger
end
RSpec.describe Burger do
it "#yummy" do
burger = double(yummy: "Yum!")
expect(burger.yummy).to eq("Yum!")
end
end
上面這個會是成功通過的測試,當面臨重構的情況,不小心移除掉某些程式碼時 ( 像是上方的 Burger 類別根本沒有 yummy 這樣的方法 ),測試依舊會通過,一切看起來安然無恙,但應用程式真的會壞掉…
所以才有了 instance doubles
的出現,他會幫助你檢查你真實的程式碼,創造彼此的連結!
用 instance doubles
來試試看!
class Burger
end
RSpec.describe Burger do
it "#yummy" do
burger = instance_double(Burger, yummy: "Yum!")
expect(burger.yummy).to eq("Yum!")
end
end
RSpec 提示錯誤了,意思就是這個類別沒辦法實施這個 yummy
實體方法,因為並沒有實作。
補上類別內的實體方法:
class Burger
def yummy
"Yum!"
end
end
RSpec.describe Burger do
it "#yummy" do
burger = instance_double(Burger, yummy: "Yum!")
expect(burger.yummy).to eq("Yum!")
end
end
通過測試啦~
整個用法就是:
something = instance_double("你要測試的類別", "他的實體方法",.....)
它就會去幫你檢查到底是不是真的有這個方法,不管你是寫多寫少,都會被抓出來,不要想要亂寫方法進去,他會知道的!
在上一些用 allow method
的範例來看看吧
class Burger
def yummy
"Yum!"
end
end
RSpec.describe Burger do
it "#yummy" do
burger = instance_double(Burger)
allow(burger).to receive(:yummy).and_return("Yum!")
expect(burger.yummy).to eq("Yum!")
end
end
其實概念就和 double
非常相似,但就是多加了一個類別上去,讓 RSpec 去幫你檢查,所以在這邊可以說,如果要使用 double
請用 instance double
,會是比較保險的做法。
等等?你問我說只有實體變數有 double
嗎?
那類別有嗎?當然有啊~
Class Doubles
我們一直強調使用 double
的情境,就是在於當你今天測試的目標會遭受其他的干擾時,我們就會利用 double
來專注在我們要測的項目上!
所以我們來創造一個使用到 class method 的情境:
class Burger
def make
"Make!"
end
end
class BurgerStore
attr_reader :burgers
def sale
@burgers = Burger.make
end
end
接著我們想要對於 BurgerStore
的 sale
方法來寫測試:
RSpec.describe BurgerStore do
it "can only implement class methods defined on a class" do
burger_klass = class_double(Burger)
expect(burger_klass).to receive(:make).and_return("Make!")
expect(subject.sale).to eq("Make!")
end
end
用我們剛剛學過關於 double
的知識來測試看看吧,一切看起來都非常合理呢~
看看是哪裡出了問題?RSpec 説期待這個 class method 應該接受到一個 make
方法。
但看看自己的原始碼,在 BurgerStore
確實有 Burger.make
這段程式碼發生啊?
哎呀,原來是因為我們還需要一個方法叫做 as_stubbed_const
接在這個 double
的後方,讓我們使用這個 double 物件去確實的取代程式碼內的 Burger.make
的這個 Burger
。
這個 as_stubbed_const
的用意就很像綁定這個 double
物件去取代真實的程式碼,這時候測試就會通過了,因為他確實接收到 make
這個方法的呼叫~
接著可以再用一段示範來看看 as_stubbed_const
的意義到底在哪裡?
class Burger
def make
"Make!"
end
end
class BurgerStore
attr_reader :burgers
def sale
@burgers = Burger.make
end
end
RSpec.describe BurgerStore do
it "can only implement class methods defined on a class" do
burger_klass = class_double(Burger).as_stubbed_const
expect(burger_klass).to receive(:make).and_return([1,2,3])
expect(subject.sale).to eq([1,2,3])
end
end
看得出來發生什麼了嗎?我們原始碼的回傳值是 "Make!"
,但現在接受的卻是 [1,2,3]
就是因為 as_stubbed_const
的綁定,但它其實和 instance_double
是一樣聰明的喔,對於你寫入的方法會有所檢查,所以不要亂寫!
結語
終於把基本的 double
都介紹了一遍,明天將會介紹 spies
也是 mock
的一種使用方式。