2014年8月10日 星期日

透過 Unity測試工具(Unity Test Tools)來進行快速的測試

原文標題:Unit testing at the speed of light with Unity Test Tools
作者: Dmitriy Mindra
原文網址:http://blogs.unity3d.com/2014/07/28/unit-testing-at-the-speed-of-light-with-unity-test-tools/


現在是時候來瞭解更多關於 NSubstitute library 以及使用它的方法。
NSubstitute library 包含在Unity測試工具(Unity Test Tools)可以在Asset Store免費下載。

每個軟體系統都由若干功能單元組成。在物件導向程式設計語言中,最小的功能單位是方法(Method)。這些方法通常都依賴於其它方法。如果你需要測試這些方法,那麼你就要面臨一些挑戰。

第一個挑戰是外部依賴的關係不容易建立,有些物件需要很複雜的初始化過程。
第二個挑戰是測試驗證某些執行路徑,需要呼叫其他的類別來驗證。
最後,呼叫外部類別方法可能會改變一些環境變數導致無法返回,例如刪除資料庫中的資料。

單元測試的作用是在一個隔離的環境中測試功能。隔離意味著所有的依賴關係項都是模擬出來的。代表測試工作在只有一個可能執行路徑的特殊環境中測試。

測試替身(Test double)技術透過在測試中替換真正的依賴對象,產生測試環境來使單元測試更加快捷和可靠。

下面要介紹5個測試替身模式:Dummy Object、Test Stub、Test Spy、Mock、Fake

Dummy Object
這裡使用了一個簡單的太空商船遊戲的複製來作為展示測試替身以及NSubstitute。這是一個以玩家控制的太空船遊戲。

玩家可以用武器裝備海盜戰艦。太空船有武器庫,武器可以安裝在任意的空武器架上。

測試模擬:確定在裝載武器的時候武器架是否已經被佔用
  • 找一個有一個空武器架的太空船
  • 找把武器
  • 裝載武器
  • 確認武器架已滿




手動建立Dummy Object來測試一下



透過基於NSubstitute 虛擬對象的測試

武器物件在這個測試場景中只是名義上的,物件的方法以及屬性在這個執行路徑中不會被使用到。它只需要一個參數,然後我們可以在所有IWeapon的介面使用這物件,如果方法沒有空指標檢查的話甚至可以使用null。那麼使用的這種物件就叫做虛擬物件模式

有兩種方式可以創建虛擬物件。

第一種方式是手動創建虛擬物件。通常每一種物件/介面應該只創建一個虛擬物件。IDE功能可以幫你產生介面。使用這樣的方法產生虛擬物件和存檔非常容易。

第二種方式是透過在Unity測試工具中的NSubstitute:



傳遞null值到沒有null參數檢查的方法中也可以被認為是虛擬物件模式。這裡就不介紹何謂Null了。上面介紹的方法都可行且易用。

Test Stub
如果太空船物件需要返回一些值,且這個值是從武器獲取的怎麼辦?在這種情況下,武器物件就不能是一個虛擬物件,就要用Test Stub

Test Stub傳回測試一個特定執行路徑的值,例如BrokenWeaponStub, IncompatibleWeaponStub以及其它可以用於測試特定場景的物件。

測試模擬: 確保太空船中裝備的可工作的武器至少發射了一發
  • 找一艘只有一個武器架的太空船
  • 裝載武器
  • 發射武器
  • 檢查太空船回合至少發射一次


透過手動建立的Test Stub來測試。
FunctionalWeaponStub 實現了IWeapon介面但是返回硬編碼(Hardcoded)的值。










與虛擬物件不同,Stub包含了硬編碼的值,用於在特定的情況下作為值來返回。

我們也可以通過NSubstitute來創建同樣的Stub


透過Substitute.For 創建的物件實現了IWeapon介面並返回一個IWeapon類型。但實際上它只是一個代理物件,它的行為是可以被修改的。Returns 是NSubstitute庫中的一個修改代理物件屬性的擴展方法,當Shoot方法被呼叫,返回在Returns()方法中指定的值。當然也可以提供一個值的序列或者委託。
返回值的序列的範例如下:


第一次呼叫任意參數的randomNumberService.Range()方法會返回0,下一次調用返回2並且依此類推。
NSubstitute中另外一個很有用的方法是可以比對範本中的參數。

在下面的範例中,預設屬性會被重寫使所有對Range(10,100)方法的調用都會返回80。更詳細的細節請參考NSubstitute手冊

在測試隨機事件中使用亂數產生的Stub是非常高效的,因為這樣可以虛擬所需要的亂數序列。
手動產生Stub非常容易,並且正確的命名可以使我們的測試程式碼更乾淨和易讀。使用NSubstitute可以減少程式碼量並且更加靈活。

Test Spy
如果我們需要記錄物件行為的怎麼辦?比如我們的對手被擊中了多少次?如果測試替身需要記錄功能,那麼就需要使用Test Spy
遭遇的代表在星際中行進的路上碰到其它物體。玩家可以在遭遇時選擇一些動作。例如如果對方是星際警察則通行,如果是海盜船則攻擊。

測試模擬:確保在遭遇中對手被擊中
  • 選擇一個對手
  • 選擇一個有兩個武器艙的太空船
  • 裝備兩個武器
  • 創建一個遭遇行為
  • 選擇遭遇中使用的攻擊動作
  • 檢查對手是否被擊中

遭遇需要兩條太空船以及一個亂數產生器來計算可能的結果。AlwaysMaxRandomNumber方法永遠返回最大值,這樣就會讓對手永遠無法避開這次打擊。這個測試走的是一個非常特殊的執行路徑。
在這次遭遇中,玩家的太空船是一個真實的物件,但是對手則是一個Spy。這個Spy包含了Stub以及記錄功能。它記錄了在測試中隨後可能檢查的擊中次數。












可以通過NSubstitute中的Arg.Do<>以及 When… Do…方法來創建Test Spy。

在傳遞參數時,NSubstitute使用特殊的參數比對器來對Arg.Do執行委派。這代表hitCount+=x.Count()會在每個方法呼叫時被執行。

Mock Object
Spy只是記錄資料,驗證就交給了開發人員。如果把驗證資料的工作交給Spy會怎麼樣?
帶驗證能力的Spy被稱為Mock Object(注:本文介紹的是非精確模擬)。
測試模擬:確保在太空船的射擊被呼叫後每一個武器都會發射
  • 找一台有兩個武器架的太空船
  • 裝備兩個武器
  • 射擊
  • 檢查是否每個武器的Shoot方法都被呼叫了
 
這個測試範例很特殊,不單是因為它使用了Mock Object模式,而且它還驗證了太空船的行為而不是狀態。(透過倫敦和芝加哥測試驅動開發學校瞭解更多)

測試替身中的測試不管返回值,它只確保特定參數值的特定方法被呼叫。這裡我們假設一個太空船使用了正確參數值的正確方法在外部系統被呼叫,那麼它的工作就是正確的。

手動創建模擬可能會很花時間。更好的選擇是使用NSubstitute來獲取Mock Object。

NSubstitute還可以驗證在一個特定的序列中方法是否被調用。



Mock Object不止記錄呼叫並且驗證呼叫。它是一個帶驗證的Test Spy。

Fake Object
如果一個測試替身需要一些邏輯怎麼辦?

包含了邏輯的測試替身叫做Fake Object,這個東西是一頭危險的野獸。這是唯一包含邏輯以及模擬真實系統的元件。Fake本身是很複雜的,它用於模擬Stub無法模擬的真實系統。常見的範例是用記憶體中資料庫替換真實的資料庫,或模擬web服務來替代真實的web服務。

最好可以能不使用Fake物件就不使用,因為它們對被替換的元件影響很大,如果沒有妥善的測試很容易發生問題。

總結
簡單的結論如下:
手動創立Dummy Object、Stub、Spy並不複雜,正確的命名可以使程式更可讀和乾淨。
NSubstitutes設計出來是用於模擬並用來更好設計模擬物件。
測試工具用來檢查行為和狀態都很好用。採用最簡單的方法。
盡可能的在測試替身中避免使用Fake(邏輯)

祝測試愉快!
範例代碼可以從GitHub上獲取。

另:

不同的書籍會使用不同的術語,請參考terminology in books。NSubstitute功能非常強大,使用前最好看一下相關文件,並遵循其中的準則。如果你仍然不清楚Fake和Stub的區別,參考文章“Mocks aren't Stubs”Arrange Act Assert(AAA) 在本文中使用來構成單元測試。這裏還有一個太空商船的開源遊戲,很值得一玩。

沒有留言:

張貼留言

著作人