項目描述:簡單演示單元測試在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
,編寫2個被測試函數,以及用於生成測試數據的函數 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
標記該測試方法重復執行指定次數