zoukankan      html  css  js  c++  java
  • 单元测试在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

    标记该测试方法重复执行指定次数

    参考文章

  • 相关阅读:
    Java StringBuilder、基本类型的包装类
    立个Flag不学好PHP誓不罢休
    LAMP搭建 转
    CentOS使用yum源中自带的rpm包安装LAMP环境
    CentOS RedHat YUM 源扩展补充(32位、64位均有)
    解决phpmyadmin上传文件大小限制的配置方法
    lanmp v2.5一键安装包发布(包括lamp,lnmp,lnamp安装)
    图像处理 jpg png gif svg
    NAT模式/路由模式/全路由模式 (转)
    网页制作中绝对路径和相对路径的区别
  • 原文地址:https://www.cnblogs.com/SouthBegonia/p/15650107.html
Copyright © 2011-2022 走看看