Website logo

Robert Chang

技術部落格

RSpec - 實用的 let 方法以及客製化錯誤訊息

昨天提到變動性的問題是什麼呢?

改變數值的時候

到現在的測試都是很單調的,也就是測一個物件本身的屬性,那如果我們實作的功能會改變屬性呢?還是可以通過測試嗎?

用昨天實作的 Ruby 方法來試試看,遇到變動會發生什麼事吧!

RSpec.describe Burger do
  def burger
    Burger.new('Beef', 'Cheddar')
  end

  it "has cheese" do
    expect(burger.cheese).to eq('Cheddar')
    burger.cheese = 'Ricotta'
    expect(burger.cheese).to eq('Ricotta')
  end

  it "has meat" do
    expect(burger.meat).to eq('Beef')
  end
end

上述測試代碼的第 8.9 行是新增上去的,根據語意可以知道我們想要改變起司的種類,在測試它的種類是不是我們新增上去的!

screen shot

咦!竟然沒有通過,可以看到錯誤訊息中,我們期待得到 Ricotta 但卻拿到 Cheddar

原因就出在於,我們呼叫的是一個方法,每呼叫一次都是一個新的物件誕生,它並不會存活在 block 裡面

所以我說用 Ruby 的方法寫沒有不行,但遇到這種會改變數值的狀況就會出錯。

那我們既不想要用 before hooks 寫這麼多行還要轉換成實體變數,又希望物件可以存活在 example 裡面!

有這麼好用的東西嗎?當然!

let helper method

提到 let 方法之前,需要提一下 Memorization 的概念,記憶的過程嗎?詳細怎麼翻譯我不太清楚,但我想講述一下概念!

今天老師上課給了你一個很難的數學題目,你花了一個小時做完,也告訴老師答案了,過了 10 分鐘,老師又給了你一模一樣的題目,你還會花一個小時做完嗎?

不,你會直接給他答案!這就是 Memorization 的概念,而 let 方法就有一點點這樣的概念存在!

先上修改好的程式碼:

RSpec.describe Burger do
  let(:burger) { Burger.new('Beef', 'Cheddar') }

  it "has cheese" do
    expect(burger.cheese).to eq('Cheddar')
    burger.cheese = 'Ricotta'
    expect(burger.cheese).to eq('Ricotta')
  end

  it "has meat" do
    expect(burger.meat).to eq('Beef')
  end
end

完成了!而且可以正確地通過測試,讓我娓娓道來!

let 的後方括號內要放的是你想要命名的變數,這邊我們放的是 :burger 記得要使用符號的形式,而後方的 block 中則是你要執行的程式碼!

let(:burger) { Burger.new('Beef', 'Cheddar')}

它會把程式碼執行完的結果,放到括號內的符號裡,而你在使用它的時候就像是區域變數一樣,超級無敵方便的啦!

還有更棒的地方就是,他會根據每一個 example 初始化,所以不需要擔心物件被污染的問題。

所以第一次呼叫 let 是在第 5 行的地方,第二次則是在第 11 行的地方,根據剛剛提到過的 Memorization,在同一個 example 中,他都會是同一個物件,也不會再次的呼叫 let 方法!

可以來測試看看,把 burger 給印出來

RSpec.describe Burger do
  let(:burger) { Burger.new('Beef', 'Cheddar') }

  it "has cheese" do
    expect(burger.cheese).to eq('Cheddar')
    p burger
    burger.cheese = 'Ricotta'
    expect(burger.cheese).to eq('Ricotta')
  end

  it "has meat" do
    expect(burger.meat).to eq('Beef')
    p burger
  end
end

可以看到結果儲存的記憶體位置是不相同的!

screen shot

before hooks 其實和這個很像,但它是在每一個 example 前都會先執行,也就是說即使你的 example 內沒有使用到,它也會執行!

let 有個很厲害的功能 Lazy-loading,也就是說當呼叫這個變數的時候,才去執行這段程式碼。

也來測試看看 before hookslet 到底差別在哪?

RSpec.describe Burger do
  let(:burger) { p 'let method' }
  before do
    p 'before hooks'
  end

  it "has cheese" do
    burger
  end

  it "has meat" do
  end
end

只有在一個 example 內呼叫了 burger 這個變數,而 burger 這個變數的內容是印出 let method 這段文字!而 before hooks 則是印出 before hooks 這段文字,讓我們來看看結果吧

screen shot

before hooks 在每一個 example 前都自動執行了,不管要或不要,但 let method 卻只有在我們呼叫的 example 內執行,另外一個則不執行!

如果我們確定這個物件是所有的 example 都需要使用的話,也可以用 let! method,但如果只有零星幾個,用 let method 可以增加效能,而且更短更好看,也不用使用實體變數

至於 let! method,加了一個驚嘆號就代表說他會在所有的 example 前執行,就跟 before hooks 一模一樣,所以說 let 真的非常強大也非常好用,基本上用這個就可以應付大多數的測試了。

客製化你的錯誤訊息

常見的錯誤訊息:

screen shot

但其實可以客製化我們想要的消息在上面,直接上程式碼再來講解!

RSpec.describe Burger do
  let(:burger) { Burger.new('Pork', 'Cheddar')}

  it "has cheese" do
    expect(burger.cheese).to eq('Cheddar')
    burger.cheese = 'Ricotta'
    expect(burger.cheese).to eq('Ricotta')
  end

  # 修改肉變成 Pork 來故意出錯,顯示客製的訊息~
  it "has meat" do
    comparison = "Beef"
    expect(burger.meat).to eq(comparison), "我想要 #{comparison} 而不是 #{burger.meat}"
  end
end

測試出錯結果:

screen shot

還記得有提過 to 是一個方法,而它除了一定要接受 expection 之外,也可以接受一些額外的選項,例如:客製化錯誤。

可以想像原本是這樣:

expect(burger.meat).to(eq(comparison), "我想要 #{comparison} 而不是 #{burger.meat}")

只是因為可以省略小括號,讓人看起來有點不直覺,但這就是 Ruby 的特色!

至於 #{} 這個寫法是很常見的在字串中安插變數的方法,也是 Ruby 基本的使用方法。

結語

今天講了很重要的 let 方法,個人覺得超級好用!

明天要開始介紹 context 這個方法以及 箝套的 describe

箝套就是 Nested,像洋蔥一樣一層一層包下去的概念

上一篇文章RSpec - 整理重複煩人的測試

下一篇文章RSpec - 用 Context 來組織程式碼區塊