單元測試在Unity中的應用


項目描述:簡單演示單元測試在Unity中的應用
項目地址:UnityTestRunner_Tutorial - SouthBegonia
項目版本:2020.3.20f1
項目用法:打開就用,代碼都放在 Assets/Editor內了

單元測試

簡介

單元測試是指對軟件中的 最小可測試單元 進行檢查和驗證,一般情況下就是對代碼中的 一個函數 去進行驗證,檢查它的 正確性
單元測試並不測基礎結構問題(如數據庫、文件系統和網絡資源的交互等)

意義

  • 節省開發期間的測試時間

​ 相比於以往直接寫業務代碼、運行Unity跑功能、看斷點看日志,單元測試能在編譯器模式下快速執行業務邏輯的單元測試

  • 有助於完善代碼

​ 因為能便捷的添加各類測試數據,所以編寫測試代碼期間就能發現正式業務代碼需要注意的地方(如判空、合法性驗證、邊界問題、算法復雜度等)

  • 減少代碼耦合

​ 當代碼緊密耦合時,可能難以進行單元測試。 如果不為編寫的代碼創建單元測試,則耦合可能不太明顯,為代碼編寫測試會自然地解耦代碼

測試模式

采用 “Arrange、Act、Assert” 模式,主要包含3個操作:

  1. 安排對象,根據需要對其進行創建和設置

  2. 作用於對象

  3. 斷言某些項按預期進行

Unity Test Runner

簡介

Unity Test Runner 是 NUnit單元測試框架 在Unity中的實現,可在編輯器模式下執行單元測試。

通過 Window->General->Test Runner 打開頁面。雙擊某測試單元或左上角的 Run All、Run Selected ... 即可執行測試,並輸出測試結果到控制台

使用流程

  1. 編寫被測試代碼

    • 被測代碼應當是剔除Unity組件交互、資源交互等后的核心算法邏輯。例如某功能模塊下的某函數
    • 若被測代碼自身已較為獨立(如各Utility類),則直接在測試代碼內調用即可;否則應當新建被測試類進行測試
    • 新建的被測試類文件可放在Asset->Editor下;采用測試功能名來命名即可
  2. 編寫測試代碼

    • 測試代碼需遵守“Arrange、Act、Assert”模式,且代碼能簡就簡
    • 測試函數需要打 [Test] 或 [TestCase] 標簽,詳見具體事例或NUnit Attribute
    • 盡量減少if、switch、for等語句的使用(減小測試代碼出bug的可能性)
    • Assert斷言語句一旦測試失敗即拋出,且失敗日志的信息較少(只知道失敗行和失敗結果),因此可輔以Debug日志或斷點調試
    • 新建的測試類文件必須放在Asset->Editor下;采用測試功能名+Tests來命名
  3. 在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

標記該測試方法重復執行指定次數

參考文章


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM