專案描述:簡單演示單元測試在Unity中的應用
專案地址:UnityTestRunner_Tutorial - SouthBegonia
專案版本:2020.3.20f1
專案用法:開啟就用,程式碼都放在 Assets/Editor內了
單元測試
簡介
單元測試是指對軟體中的 最小可測試單元 進行檢查和驗證,一般情況下就是對程式碼中的 一個函式 去進行驗證,檢查它的 正確性。
單元測試並不測基礎結構問題(如資料庫、檔案系統和網路資源的互動等)。
意義
- 節省開發期間的測試時間
相比於以往直接寫業務程式碼、執行Unity跑功能、看斷點看日誌,單元測試能在編譯器模式下快速執行業務邏輯的單元測試
- 有助於完善程式碼
因為能便捷的新增各類測試資料,所以編寫測試程式碼期間就能發現正式業務程式碼需要注意的地方(如判空、合法性驗證、邊界問題、演算法複雜度等)
- 減少程式碼耦合
當程式碼緊密耦合時,可能難以進行單元測試。 如果不為編寫的程式碼建立單元測試,則耦合可能不太明顯,為程式碼編寫測試會自然地解耦程式碼
測試模式
採用 “Arrange、Act、Assert” 模式,主要包含3個操作:
-
安排物件,根據需要對其進行建立和設定
-
作用於物件
-
斷言某些項按預期進行
Unity Test Runner
簡介
Unity Test Runner 是 NUnit單元測試框架 在Unity中的實現,可在編輯器模式下執行單元測試。
通過 Window->General->Test Runner 開啟頁面。雙擊某測試單元或左上角的 Run All、Run Selected ... 即可執行測試,並輸出測試結果到控制檯
使用流程
-
編寫被測試程式碼
- 被測程式碼應當是剔除Unity元件互動、資源互動等後的核心演算法邏輯。例如某功能模組下的某函式
- 若被測程式碼自身已較為獨立(如各Utility類),則直接在測試程式碼內呼叫即可;否則應當新建被測試類進行測試
- 新建的被測試類檔案可放在Asset->Editor下;採用測試功能名來命名即可
-
編寫測試程式碼
- 測試程式碼需遵守“Arrange、Act、Assert”模式,且程式碼能簡就簡
- 測試函式需要打 [Test] 或 [TestCase] 標籤,詳見具體事例或NUnit Attribute
- 儘量減少if、switch、for等語句的使用(減小測試程式碼出bug的可能性)
- Assert斷言語句一旦測試失敗即丟擲,且失敗日誌的資訊較少(只知道失敗行和失敗結果),因此可輔以Debug日誌或斷點除錯
- 新建的測試類檔案必須放在Asset->Editor下;採用測試功名+Tests來命名
-
在Unity Test Runner 頁面執行目標測試
- 選中較為常用的EditMode
- 選中各自需測試的單元執行測試即可(如某個測試類或該測試類下的某測試函式)
具體事例
事例1
需要測試GameUtils
類下的獲取字串長度函式GetTextLength()
,在各類傳參下能否返回正確長度值。
先新建被測試類GameUtils
及被測試函式GetTextLength()
:
public class GameUtils
{
public static int GetTextLength(string str)
{
// ---------- 錯誤:缺判空 ----------
// if (string.IsNullOrEmpty(str))
// {
// return 0;
// }
int len = 0;
for (int i = 0; i < str.Length; i++)
{
byte[] byte_len = Encoding.UTF8.GetBytes(str.Substring(i, 1));
if (byte_len.Length > 1)
len += 2;
else
len += 1;
}
return len;
}
}
後新建GameUtils
的測試用類GameUtilsTests
,編寫GetTextLength()
的測試函式:
public class GameUtilsTests
{
// GetTextLength測試null字串
[Test]
public void GetTextLength_NullStr()
{
string str = null;
int result = GameUtils.GetTextLength(str);
Assert.AreEqual(0, result);
}
// 多測試資料的GetTextLength測試
[TestCase("", 0)]
[TestCase("Hello World", 11)]
public void GetTextLength_MultiTestData(string data, int exResult)
{
int result = GameUtils.GetTextLength(data);
Assert.AreEqual(exResult, result);
}
}
測試結果如下:
事例2
需要測試PVP排行榜的排序演算法,是否能在單、多排序引數下正確得到排序資料。
先簡化排行榜資料單元類為PVPRankCell
,新建被測試類PVPRankSort
,編寫測試函式PVPRankCellComparer_BySingleComparedParam()
、PVPRankCellComparer_ByMultiComparedParam()
,以及用於生成測試資料的方法GenTestRankList()
:
//排行榜資料單元
public class PVPRankCell
{
public string Name;
public int Score;
public int RankInGlobal;
public long PlatformID;
}
public class PVPRankSort
{
public static int PVPRankCellComparer_BySingleComparedParam(PVPRankCell a, PVPRankCell b)
{
//return -a.PlatformID.CompareTo(b.PlatformID); //錯誤
return a.PlatformID.CompareTo(b.PlatformID); //正確
}
public int PVPRankCellComparer_ByMultiComparedParam(PVPRankCell a, PVPRankCell b)
{
if (a.Score != b.Score)
return -a.Score.CompareTo(b.Score);
if (a.RankInGlobal != b.RankInGlobal)
return a.RankInGlobal.CompareTo(b.RankInGlobal);
return -a.PlatformID.CompareTo(b.PlatformID); //錯誤
//return a.PlatformID.CompareTo(b.PlatformID); //正確
}
// 生成測試用資料
public List<PVPRankCell> GenTestRankList()
{
List<PVPRankCell> testRankList = new List<PVPRankCell>
{
new PVPRankCell() {Name = "A", Score = 10, RankInGlobal = 3, PlatformID = 1001},
new PVPRankCell() {Name = "B", Score = 10, RankInGlobal = 3, PlatformID = 1002},
new PVPRankCell() {Name = "C", Score = 10, RankInGlobal = 3, PlatformID = 1002}, //隱患資料
new PVPRankCell() {Name = "D", Score = 20, RankInGlobal = 1, PlatformID = 1003},
new PVPRankCell() {Name = "E", Score = 30, RankInGlobal = 2, PlatformID = 1004},
};
return testRankList;
}
}
後新建測試類PVPRankSortTests,編寫2個排序演算法的測試函式:
public class PVPRankSortTests
{
PVPRankSort PvpRankSort;
[SetUp]
public void SetUp()
{
//最先執行的方法,作為多測試方法的功能部分
PvpRankSort = new PVPRankSort();
}
[TearDown]
public void TearDowm()
{
//最後執行的方法,用於清除或回收公共資源
PvpRankSort = null;
}
// 單一比較引數排序演算法的測試
[Test]
public void PVPRankSort_SingleComparedParam()
{
// Arrange:安排物件,根據需要對其進行建立和設定
// 如構造測試用資料
List<PVPRankCell> testRankList = PvpRankSort.GenTestRankList();
// Act:作用於物件
// 如具體演算法實現
testRankList.Sort(PVPRankSort.PVPRankCellComparer_BySingleComparedParam);
// Assert:斷言某些項按預期進行
// 如結果校驗:PlatformID升序
for (int index = 0; index + 1 < testRankList.Count; ++index)
{
if (testRankList[index].PlatformID != testRankList[index + 1].PlatformID)
Assert.Less(testRankList[index].PlatformID, testRankList[index + 1].PlatformID); //PlatformID升序
else
Debug.LogWarning($"Warning>>>>> {testRankList[index].Name} 的排序引數和 {testRankList[index + 1].Name} 一致"); //隱患情況
}
}
// 多比較引數排序演算法的測試
[Test]
public void PVPRankSort_MultiComparedParam()
{
// Arrange:安排物件,根據需要對其進行建立和設定
// 如構造測試用資料
List<PVPRankCell> testRankList = PvpRankSort.GenTestRankList();
// Act:作用於物件
// 如具體演算法實現
testRankList.Sort(PvpRankSort.PVPRankCellComparer_ByMultiComparedParam);
// Assert:斷言某些項按預期進行
// 如結果校驗:分數降序->名次升序->PlatformID升序
for (int index = 0; index + 1 < testRankList.Count; ++index)
{
if (testRankList[index].Score != testRankList[index + 1].Score)
Assert.Greater(testRankList[index].Score, testRankList[index + 1].Score); //分數降序
else if (testRankList[index].RankInGlobal != testRankList[index + 1].RankInGlobal)
Assert.Less(testRankList[index].RankInGlobal, testRankList[index + 1].RankInGlobal); //排名升序
else if (testRankList[index].PlatformID != testRankList[index + 1].PlatformID)
Assert.Less(testRankList[index].PlatformID, testRankList[index + 1].PlatformID); //PlatformID升序
else
Debug.LogWarning($"Warning>>>>> {testRankList[index].Name} 的排序引數和 {testRankList[index + 1].Name} 一致"); //隱患情況
}
}
}
測試結果如圖:
其他
NUnit Attribute
TestAttribute
常用標籤,標記該方法能被執行測試,方法必須為public void 無參
// GetTextLength測試null字串
[Test]
public void GetTextLength_NullStr()
{
string str = null;
int result = GameUtils.GetTextLength(str);
Assert.AreEqual(0, result);
}
TestCaseAttribute
標記該方法能被執行測試,方法必須為public void,可傳參,引數由TestCase傳入
// 多測試資料的GetTextLength測試
[TestCase("", 0)]
[TestCase("Hello World", 11)]
public void GetTextLength_MultiTestData(string data, int exResult)
{
int result = GameUtils.GetTextLength(data);
Assert.AreEqual(exResult, result);
}
TestFixtureAttribute
暫無需使用。用於標記一個類為測試類,其中此類必須是public,必須保證此建構函式不能有任何的副作用(不能出現異常或者錯誤的情況),在一個測試過程中,可以被構造多次。如果建構函式帶有引數,可以指定預設的初始化引數
SetUpAttribute
標記該方法在測試流程中被首先執行,用作初始化公共引數
PVPRankSort PvpRankSort;
[SetUp]
public void SetUp()
{
//最先執行的方法,作為多測試方法的功能部分
PvpRankSort = new PVPRankSort();
}
TearDownAttribute
標記該方法被最後執行,用作回收公共引數部分,與SetUp配對使用
[TearDown]
public void TearDowm()
{
//最後執行的方法,用於清除或回收公共資源
PvpRankSort = null;
}
CategoryAttribute
給該測試方法打篩分標籤,在UnityTestRunner頁面可篩分顯示(但有特殊字元限制)
RepeatAttribute
標記該測試方法重複執行指定次數