一、單元測試的定義與作用
單元測試定義:單元測試在傳統軟件開發中是非常重要的工具,它是指對軟件中的最小可測試單元進行檢查和驗證,一般情況下就是對代碼中的一個函數去進行驗證,檢查它的正確性。一個單元測試是一段自動化的代碼,這段代碼調用被測試的工作單元,之后對這個單元的單個最終結果的某些假設進行檢驗。單元測試使用單元測試框架編寫,並要求單元測試可靠、可讀並且可維護。只要產品代碼不發生變化,單元測試的結果是穩定的。(百度的)
單元測試可以讓你在軟件開發的早期階段發現 Bug,而不必到集成測試的時候才發現,開發完成一個模塊(類、函數)就對應地做一個單元測試,盡早發現並處理掉bug,提高代碼的質量。(反正單元測試就是杠杠好!)
二、在Unity中使用NUnit進行單元測試
話說,馬三在工作的過程中,極少地發現周圍的同事會對自己編寫功能進行單元測試。一般都是開發完功能以后,隨便寫兩段測試的代碼(有的甚至都不測一下),一看沒有問題就丟到SVN或者Git倉庫里面了。結果當游戲出包以后,測試團隊總會反饋回很多完全可以提前規避掉的低級bug。當然可能他們本身沒有單元測試的習慣或者由於活多、工期太緊等種種原因,才不做單元測試的。(活都要干不完了,還做毛測試,Delay不扣錢啊!?)
好了,閑話扯完該說說咱們這個單元測試了。單元測試目前有很多成熟的框架可以供我們使用,我比較推薦的就是Unity Editor自帶的Editor Tests Runner,功能不多,但是已經夠用了,使用也很方便。Editor Tests Runner是開源單元測試工具NUnit在Unity引擎中的實現,目前Unity中使用的NUnit版本是2.6.4。
Editor Tests Runner可以通過Window -> Editor Tests Runner菜單打開,它的樣子如下圖所示:
在這個窗口中顯示了當前添加的單元測試用例,以及他們通過的情況。首先,你需要點擊窗口左上角的Run All按鈕來執行所有的單元測試。綠色的對號表示這個用例通過了單元測試,紅色的禁止符號表示未通過單元測試。
下面我們來看一下如何編寫單元測試的代碼。單元測試代碼和游戲運行時代碼是分開保存的,它只在Editor環境下可用,因此你需要把它放到Editor目錄下。
首先為了下面的測試,我們先定義一個自定義類型的錯誤異常,提前備用。只需要讓它直接繼承 ApplicationException 就可以了,代碼如下所示:
using System; using System.Collections.Generic; using System.Linq; using System.Text; /// <summary> /// 自定義的異常類型 /// </summary> class NegativeHealthException : ApplicationException { }
下面編寫我們的需要進行被測試的模塊或者代碼。假設游戲代碼中存在一個Player類來代表主角色,里面有幾個函數用來在玩家受到傷害時減少血量,或者通過葯水回復血量。其中Damage函數寫了三個版本,一個是正確的,兩個是返回錯誤結果的。在正確的函數中,當 Health 的值小於 100 的時候,會拋出一個剛才我們自定義的異常。代碼如下所示:
1 using System.Collections; 2 using System.Collections.Generic; 3 using UnityEngine; 4 5 public class Player{ 6 7 public float Health { get; set; } 8 9 #region 正確的方法 10 11 public void Damage(float value) 12 { 13 Health -= value; 14 if (Health < 0) 15 { 16 throw new NegativeHealthException(); 17 } 18 } 19 20 public void Recover(float value) 21 { 22 Health += value; 23 } 24 #endregion 25 26 #region 錯誤的方法 27 28 public void DamageWrong(float value) 29 { 30 Health -= value + 1; 31 } 32 33 public void DamageNoException(float value) 34 { 35 Health -= value; 36 } 37 #endregion 38 }
接着我們來編寫單元測試代碼。這里我們創建了一個叫做PlayerTest的類,里面寫了兩個函數分別代表兩個測試用例。為了讓Unity識別這兩個函數是測試用例,我們需要在函數前加上 [Test] 的屬性,這樣所有帶有 [Test] 屬性的函數都會成為一個測試用例,代碼如下。
1 using System.Collections; 2 using System.Collections.Generic; 3 using NUnit.Framework; 4 using UnityEngine; 5 6 public class PlayerTest{ 7 8 [Test] 9 public void TestHealth() 10 { 11 Player player = new Player(); 12 player.Health = 1000.0f; 13 14 //通過Assert斷言來判斷這個函數的返回結果是否符合預期 15 player.Damage(200); 16 Assert.AreEqual(800,player.Health); 17 18 player.Recover(150); 19 Assert.AreEqual(950,player.Health); 20 } 21 22 [Test] 23 [ExpectedException(typeof(NegativeHealthException))] 24 public void NegativeHealth() 25 { 26 Player player = new Player(); 27 player.Health = 1000; 28 29 player.Damage(500); 30 player.Damage(600); 31 } 32 33 }
相信大家在寫到 [ExpectedException(typeof(NegativeHealthException))] 的時候,VS肯定會報紅,提示找不到 ExpectedException 這個標簽,這是因為,ExpectedException這個標簽是屬於VS的單元測試的內容,在 NUnit.Framework 這個命名空間中,因此我們還需要使用 using NUnit.Framework; 來引入VS的單元測試模塊。但是如果你會發現這個模塊無法引入,VS沒有自動補全這個命名空間,就算手動寫上了還是提示找不到。這是為什么呢?
眾所周知,Unity的.NET是基於 Mono 的,因為一些原因,導致Mono並不是包含了所有微軟原生的.NET庫中的內容。也就是說有些你在Winform、WPF等工程中用到的類庫並不能完美地在Mono中使用,這也就是為什么會發生上述找不到單元測試的模塊的問題。其實這個問題也很好解決,我們只要把 VS 中的單元測試模塊的DLL找到(名為 Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll ),手動拷貝到Unity工程中,再在IDE里面引入它就可以使用了。具體的操作步驟如下:
1.找到VS中的單元測試模塊DLL的所在位置,經過在 Stackoverflow 上面查詢,我們得知Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll 一般在 C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\PublicAssemblies\Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll 路徑下。如下圖所示:
2.把這個DLL手動拷貝到Unity的工程中,並在我們的解決方案中引用它。在我們的Unity項目中新建一個名為 “Plugins” 的文件夾,然后把上面的Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll 拷貝到該文件夾下,再重新打開我們的VS解決方案,就可以發現,這個模塊已經自動被引用進來了,之后就可以放心地使用單元測試相關的代碼了。
一般在傳統的C#項目中,我們引用某個DLL的時候,都是通過在VS解決方案的引用項目上右鍵 -> 添加新引用來導入某個DLL,但是在Unity的項目中,我們在引用選項上右鍵卻發現沒有這個選項。其實,只要像上述的那樣直接把dll 拷貝到 "Plugins"目錄下,VS就會自動把DLL引用到我們的項目中了,非常方便。
在上面的測試函數中,假如我們想測試Damage這個函數是否正常工作,需要使用 Assert.AreEqual 來判斷這個函數的返回結果是否與預期的結果一致。如果Assert.AreEqual判斷結果是正確的,就會在Tests Runner窗口中用一個綠色的對號表示這個測試通過了,反之就會用紅色的禁止符號表示失敗。第二個名為 NegativeHealth 測試用例函數,是用來判斷判斷這個函數有沒有正常地拋出異常,如果沒有按照預期拋出異常也會被認為是失敗的測試用例。如果你需要捕獲拋出異常與你的預期值是否一致,還需要在函數前添加另外一個屬性 [ExpectedException(typeof(NegativeHealthException))],這樣這段代碼就會判斷拋出的異常是否正確了。通過下圖可以看到,我們所編寫的兩個測試函數用例都通過了,顯示為綠色。
這時候大家可能發現了,上面的腳本對應了測試結果中PlayerTest這一部分,另外還有一個PlayerTestWrong的分組並沒有出現。這是因為我們可以在Editor目錄下添加多個測試腳本,這些測試腳本可以測試同一個模塊的代碼,也可以同時測試不同模塊的代碼。下面讓我們來看一下PlayerTestWrong的腳本如何編寫,它的內容和剛才的測試代碼非常相似,只不過調用了返回錯誤值的函數。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using NUnit.Framework; 6 7 8 class PlayerTestWrong 9 { 10 [Test] 11 public void TestHealthWrong() 12 { 13 Player player = new Player(); 14 player.Health = 1000.0f; 15 16 player.DamageWrong(200); 17 Assert.AreEqual(800,player.Health); 18 19 player.DamageWrong(150); 20 Assert.AreEqual(950,player.Health); 21 } 22 23 [Test] 24 [ExpectedException(typeof (NegativeHealthException))] 25 public void NegativeHealthNoException() 26 { 27 Player player = new Player(); 28 player.Health = 1000; 29 30 player.DamageNoException(500); 31 player.DamageNoException(600); 32 } 33 }
Editor Tests Runner的基本使用方法介紹到這里就結束了,下面介紹一個小技巧。如果你想實現全自動的單元測試的話,可能會考慮使用批處理來自動化執行測試,為此Unity也提供了批處理的方式。如果你需要使用這個功能的話,只需要在運行Unity的時候傳入以下參數,每個參數的含義請查看 Unity官方文檔 ,本篇博客中就不進行介紹了。
- runEditorTests
- editorTestsResultFile
- editorTestsFilter
- editorTestsCategories
- editorTestsVerboseLog
三、小結
對於游戲開發者來說,單元測試可能比較陌生。不過現在隨着游戲復雜度的逐漸提升,另外很多有一定規模的公司都會同時開發多個項目。我們會發現其實有很多功能都被封裝為通用的工具庫。在這種情況下如果我們再不重視代碼的質量,就會導致一個Bug可能同時影響多個項目的開發進度。因此我們還是建議在時間允許的情況下,對比較重要的模塊,以及重用性比較高的代碼增加單元測試。
最后放上本篇博客中演示的項目源碼:
Github地址:https://github.com/XINCGer/Unity3DTraining/tree/master/Unit4Unity/Editor%20Test%20Runner 歡迎fork!
引用資料:
作者:馬三小伙兒
出處:http://www.cnblogs.com/msxh/p/7354229.html
請尊重別人的勞動成果,讓分享成為一種美德,歡迎轉載。另外,文章在表述和代碼方面如有不妥之處,歡迎批評指正。留下你的腳印,歡迎評論!