[單元測試] Fake依賴物件的兩個小陷阱

  • 178
  • 0
  • 2017-08-06

單元測試透過Fake物件破除依賴時,有兩個小細節須注意。

  1. 要記得定義與Fake物件互動的方法的回傳值。
  2. 定義的回傳值須注意實例化的方式是否符合測試情境需求。

 

前言

之前在寫測試Code的時候,有遇到兩個案例,明明Production Code都正常執行,但測試案例卻始終紅燈的問題。最後有發現是因為觀念不清楚,而造成很多時間的浪費,為了怕以後自己又忘記,趕緊把它紀錄下來。

 

只有套好的招才算數

在寫單元測試時,第一個遇到的挑戰就是要如何破除依賴,破除依賴的方式可以透過撰寫手工類別,注入測試的目標物件來達到。也可以透過第三方套件NSubstitute達到同樣的效果。簡單來說就是Fake依賴的物件,並給他一個預期回傳的結果,而不去真正的執行商業邏輯。

如果是選擇使用NSubstitute套件,我們可透過NSubstitute提供的API來產生Fake物件,因為是依賴介面,所以只要是介面有定義的方法,都可以被呼叫,但Fake物件不會真的去跑商業邏輯。

這時候第一個疑問一定是,如果Fake物件不會去跑商業邏輯,那如何得到執行結果?假如被呼叫的方法沒有回傳值,那基本上不會有甚麼問題,不需要做甚麼額外的事情;但如果有回傳值的話,則必須由測試人員根據模擬的情境來套招。講白話就是,我們要跟Fake物件約定好,當我呼叫你的AAA方法,傳入XXX參數,請回傳YYY給我。

套好招之後,接下來跑測試,Production Code照理來說會按照預期的邏輯,呼叫Fake物件的AAA方法,並傳入XXX參數,就會收到YYY的回傳值。假如Production Code沒有依照你的想法執行,那就要確認Production Code是否有符合你情境描述的需求。

所以,這裡第一個觀念,就是要能識別出預期的商業邏輯中,有跟Fake物件互動到哪些方法,如果方法有回傳值,一定都要先跟Fake物件做約定。否則,呼叫到沒跟Fake物件約定好的方法,那Fake物件會一律回你一個null。

 

Fack物件套招時須注意如何實例化回傳的對象

一般來說,Production Code在跟Fake物件互動時,要注意互動的次數,是否跟你套招時,實體化物件的次數相等,假如不相等,就要檢查實體化物件的方式是否符合你的需求,否則有可能就會出現問題,這樣也許太攏統,直接來看範例就能了解。

假設我們要測試的功能情境是"汽車生產後,幫汽車掛上車牌",測試目標對象是CarService,依賴ICarFactory,物件的依賴關係和定義如下圖所示。

我們的測試情境如下(透過Specflow撰寫),由CarService的ProduceCar方法,透過ICarFactory生產汽車,剛生產的車子只會包含品牌屬性,不含車牌。接著假設我們擁有三組車牌資訊,然後呼叫CarService的HangPlate方法掛上車牌,最後驗證我們的車牌是不是都有掛到剛生產的汽車上。

Scenario: 替剛出產的汽車掛上車牌
	Given 我預期ICarFactory生產的汽車屬性如下(ICarFactory是stub物件)
	| Brand  | Plate |
	| Luxgen |       |
	And  我有車牌資料"ABC-0010","ABC-0011","ABC-0012"
	When 我生產3輛車放入倉庫 (CarService.ProduceCar)
	And  替倉庫的車子掛上車牌 (CarService.HangPlate)
	Then 則倉庫內車子屬性應為
	| Brand  | Plate    |
	| Luxgen | ABC-0010 |
	| Luxgen | ABC-0011 |
	| Luxgen | ABC-0012 |

這裡只秀出第一行Give的邏輯。

[Given(@"我預期CarFactory生產的汽車屬性如下")]
public void Given我預期CarFactory生產的汽車屬性如下(Table table)
{
    var car = table.CreateInstance<CarViewModel>();
    this.carFactoryService.ProduceCar().Returns(car );
}

這段邏輯很簡單,看起來沒有問題,但跑出來的測試結果是錯誤的。

我們可以看到車牌全部都變成ABC-0012,可是真的跑Production Code,卻又沒有問題(假設Production Code真的是正確的),但這個測試案例就是通不過!

錯誤的理由是,因為我們跟Fake物件套招時只有實例化汽車1次。但生產3輛車,其實Fake物件被叫用了3次,所以造成Fake物件都是回傳同一輛車。所以,在倉庫內雖然看起來有3輛車,但這3輛車的參考都指向同一個記憶體位置(因為都是透過Fake物件ICarFactory產生的),所以依序在幫汽車掛車牌的時候,就會一直覆蓋原來的車牌,造成3輛車的車牌都是最後一個車牌,ABC-0012。

那樣怎麼避免這個問題呢?我們的需求很單純,只是要不同的車子指向不同的物件而已,所以,透過呼叫另外一個多載方法,傳一個Func進去,讓Fake物件每次套招的時候都產生一個新的物件,實作方式如下。

[Given(@"我預期CarFactory生產的汽車屬性如下")]
public void Given我預期CarFactory生產的汽車屬性如下(Table table)
{
    //用全域私有變數接住table 下面會需要用到這個資訊
    this.NewCarTable = table;

    //傳入Func  由Func來實例化CarViewModel 就能避免物件都指向同一個記憶體位置
    this.carFactoryService.ProduceCar().Returns(this.ProduceCar);
}

private CarViewModel ProduceCar(CallInfo arg)
{
    //實例化汽車物件
    return this.NewCarTable.CreateInstance<CarViewModel>();
}

這樣就大功告成了,測試案例也會出現綠燈。

 

總結

透過TDD的方式來開發系統可以享受到程式碼被保護的好處,以及商業流程可以用口語化的方式記錄下來,不用在從冷冰冰的程式碼中兜出商業邏輯。

但要寫出好的測試Code,基本的套路技巧,如破除依賴,如何具體描述測試案例都需要經驗來學習,尤其是破除依賴這一塊,有很多小陷阱要避免,也有很多小技巧可以運用,一開始也許會有點痛苦,但熟悉之後,就會慢慢體驗到TDD的浪漫。