還記得一開始提到的 unit test,我們希望著重在小的功能上進行測試,但一個應用程式常常具有各式各樣不同的依賴。
這時候的測試就會變得很難寫,因為需要顧慮到許多不同的方法所回傳的值,它們都會影響到測試的結果!
這時候 mock
系列的功能就可以很有效地幫助到你,因為它就是為了假裝、為了模仿而生!
Double method
從概念上來說,double
是一個製造假物件的方法,而這個假物件進而能夠接受方法,設定好回傳值。
先看看範例,再來解釋:
RSpec.describe "double method" do
it "can defined method to be invoked" do
basketball_player = double("Lebron James", dunk: "Ah!!!!", shoot: "Goal!!!!")
expect(basketball_player.dunk).to eq("Ah!!!!")
end
end
我們 double
出了一個籃球員,可以灌籃然後發出 Ah!!!
的聲音~
上面這段測試是成功通過的!那到底是在做什麼呢?
double
可以讓我們預期的方法和回傳的值,變成 key-value
的組合,然後存放在這個 double
物件中。
接著一樣可以使用他身上的方法,然後得到預設好的回傳值~
也可以用 allow
的寫法,看起來會更直觀一些,雖然比較長。
RSpec.describe "double method" do
it "can defined method to be invoked" do
basketball_player = double("Lebron James")
allow(basketball_player).to receive(:dunk).and_return("Ah!!!!")
expect(basketball_player.dunk).to eq("Ah!!!!")
end
end
我們允許
basketball_player
物件接收一個dunk
方法,然後回傳Ah!!!!!
所以這個測試也是理所當然的會成功通過!
但這樣一次寫一個會不會太麻煩了?而且又不想要寫在初始化裡面很亂…
還有另一個叫做 receive_messages
的方法喔!
RSpec.describe "double method" do
it "can defined method to be invoked" do
basketball_player = double("Lebron James")
allow(basketball_player).to receive_messages(dunk: "Ah!!!!", shoot: "Goal!!!!")
expect(basketball_player.dunk).to eq("Ah!!!!")
expect(basketball_player.shoot).to eq("Goal!!!!")
end
end
其實效果等同你在初始化的時候,直接寫在後面是一樣的道理,但我自己比較喜歡 allow
的方法,會讓人比較理解的感覺。
但其實上面的三種方式都可以,都是建立 double
物件的手段~
而或許你還是覺得,所以這能幹嘛?反正怎麼測都可以過,有意義嗎?
當然不是叫你針對所有要測試的物件做 double
,這樣錢也太好賺了吧!
而是當測試目標被不同的方法所依賴時才導致我們需要 double
的幫忙。
接下來就會用範例來看看,什麼叫被不同的方法所依賴給影響,導致需要使用 double
的情境。
class Cowboy
def initialize(name)
@name = name
end
def fighting?
true
end
def draw_the_gun
"Bang!!!"
end
def be_shot
"Help me..."
end
def continue?
false
end
end
class Bar
attr_reader :cow_boy
def initialize(cow_boy)
@cow_boy = cow_boy
end
def start_fighting
if cow_boy.fighting?
cow_boy.draw_the_gun
cow_boy.be_shot
cow_boy.continue?
end
end
end
從上面這個簡單的西部牛仔劇情片編織的類別,可以看到酒吧以及牛仔之間的關係!
但當我們今天要寫 Bar
這個類別的測試時怎麼辦呢?
我們要測試 start_fighting
這個方法時,可以清楚地看到受到 cow_boy
的依賴。
但其實我們根本可以不用關心 cow_boy
是怎麼作業的?它的方法回傳什麼那都不重要,我們要專注在現在這個方法應該要怎麼通過才對。
他的流程是不是正確的?這才是我們在乎的地方。
所以這就是前面提到,被影響到的時候,我們可以用 double
來取代這邊的 cow_boy
然後使測試可以通過!
當然這是很搞笑的介紹,但事實上的專案中,就是會有許多不同的關係的存在,也都會影響到整個方法的進行。
接著來用 double
測試這個 Bar
類別的實體方法吧!
RSpec.describe Bar do
let(:cow_boy) { double("Gene Autry", fighting?: true, draw_the_gun: "Bang!!!", be_shot: "Help me...", continue?: true) }
subject { described_class.new(cow_boy) }
describe "#start_fighting method" do
it "expect cow_boy start_fight" do
expect(cow_boy).to receive(:fighting?)
expect(cow_boy).to receive(:draw_the_gun)
expect(cow_boy).to receive(:be_shot)
expect(cow_boy).to receive(:continue?)
subject.start_fighting
end
end
end
這是會成功通過的測試,因為我們在測試中期待這個物件會接收到這些方法。
但如果我們不給予這些方法的話,這個測試是會沒辦法成功運作的喔!
因為我們把 double
物件放進去 Bar
類別生成實體,進而執行它的 start_fighting
這個方法,其中有使用到的方法都會先去參考 double
物件,存不存在才繼續向下執行!
可以看看如果我們只寫了 double
物件的話:
RSpec.describe Bar do
let(:cow_boy) { double("Gene Autry") }
subject { described_class.new(cow_boy) }
describe "#start_fighting method" do
it "expect cow_boy start_fight" do
subject.start_fighting
end
end
end
再看看執行的 Output !
程式的執行在 if cow_boy.fighting?
中斷了,內容也有提到我們的 double
物件沒有接受到這個方法。
所以可以用最上面介紹的三種寫法來填入方法,使我們可以專注在測試 Bar
的方法,而不受到 cow_boy
影響!
而如果想要指定接收次數的話,也可以加入計數的方法喔!
畢竟某些時候你會想要限制有些方法只執行一次,或是最多兩次等等的條件~
describe "#start_fighting method" do
it "expect cow_boy start_fight" do
expect(cow_boy).to receive(:fighting?)
expect(cow_boy).to receive(:draw_the_gun)
expect(cow_boy).to receive(:be_shot).twice
# expect(cow_boy).to receive(:be_shot).exactly(1).times
# expect(cow_boy).to receive(:be_shot).at_most(1).times
expect(cow_boy).to receive(:continue?)
subject.start_fighting
end
end
上面有註解的部分,都是可以用的寫法!
現在我們加上了 twice
就是希望這個方法能被執行兩次,那我們先來看看 Output
這邊的意思就是,RSpec 期待會收到兩次的執行,但其實程式碼只有一次!
那先改寫要測的的方法,讓牛仔中槍兩次吧
def start_fighting
if cow_boy.fighting?
cow_boy.draw_the_gun
cow_boy.be_shot
cow_boy.be_shot
cow_boy.continue?
end
end
這樣就能成功通過測試囉,但其實不寫任何次數的話也是會通過啦!
但既然是寫測試,方法會執行幾次,或是接收什麼樣的參數,都是需要考量和注意的!
結語
今天介紹了使用 double
的情境,以及為什麼要使用它。
就是希望可以讓我們的工作更順利,不要在一些奇怪的事情上煩惱,有時候被其他的物件干擾導致測試寫不出來,真的是超級痛苦的!
明天會稍微介紹一下今天有提到的 allow method
然後進入 instance double
的世界!
這是一個更真實更好用的東西!