Website logo

Robert Chang

技術部落格

RSpec - Instance Doubles

昨天提過的 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 實體方法,因為並沒有實作。

screen shot

補上類別內的實體方法:

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

通過測試啦~

screen shot

整個用法就是:

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

接著我們想要對於 BurgerStoresale 方法來寫測試:

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 的知識來測試看看吧,一切看起來都非常合理呢~

screen shot

看看是哪裡出了問題?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 的一種使用方式。

上一篇文章RSpec - Double Object

下一篇文章RSpec - Spy 在測試中的間諜