项目描述:简单演示单元测试在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,编写测试函数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

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

参考文章

单元测试在Unity中的应用的更多相关文章

  1. Keil中搭建自动化单元测试框架Unity

    前言: 虽然一些C++的自动化单元测试框架也能用来C语言单元测试,但那样我们编写C语言程序时需要符合C++的标准,这样有一些C的特性是无法使用的,限制C的特性使用不太好,于是找了一个全部用C实现的自动 ...

  2. Unity中使用Attribute

    Attribute是c#的语言特性 msdn说明如下: The Attribute class associates predefined system information or user-def ...

  3. 在Unity中使用UGUI修改Mesh绘制几何图形

    在商店看到这样一个例子,表示很有兴趣,他们说是用UGUI做的.我想,像这种可以随便变形的图形,我第一个就想到了网格变形. 做法1: 细心的朋友应该会发现,每个UGUI可见元素,都有一个‘Canvas ...

  4. Unity中使用WebView

    Unity中使用WebView @(设计) 需求,最近游戏中需要引入H5直播页面和更新比较频繁的赛事页面,需求包括:加密传参数.和Unity交互,在Unity框架下其实有几种方案: 内置函数Appli ...

  5. Unity中创建二维码

    在网络上发现了一个可以把字符串转换成二维码的dll,但是我们要怎么使用他呢.不废话,直接进入主题. 用到的引用 using UnityEngine;using ZXing;using ZXing.Qr ...

  6. 在Unity中创建可远程加载的.unity3d包

    在一个Unity项目中,发布包本身不一定要包括所有的Asset(译为资产或组件),其它的部分可以单独发布为.unity3d,再由程序从本地/远程加载执行,这部分不在本文讨论范围.虽然Unity并没有直 ...

  7. 【原创翻译】初识Unity中的Compute Shader

    一直以来都想试着自己翻译一些东西,现在发现翻译真的很不容易,如果你直接把作者的原文按照英文的思维翻译过来,你会发现中国人读起来很是别扭,但是如果你想完全利用中国人的语言方式来翻译,又怕自己理解的不到位 ...

  8. 【unity shaders】:Unity中的Shader及其基本框架

    shader和Material的基本关系 Shader(着色器)实际上就是一小段程序,它负责将输入的Mesh(网格)以指定的方式和输入的贴图或者颜色等组合作用,然后输出.绘图单元可以依据这个输出来将图 ...

  9. Unity中各个平台的预编译的运用方式

    1,unity中官方文档的一个操纵关键词   Platform Dependent Compilation 2,常用的预编译关键词    UNITY_EDITOR    编辑器调用.UNITY_STA ...

随机推荐

  1. matlab添加永久路径

    addpath('D:\MATLAB6p5\toolbox\svm'); 临时添加路径,不能添加子目录 addpath(genpath('D:\MATLAB6p5\toolbox\svm'));临时添 ...

  2. 广域网(ppp协议、HDLC协议)

    文章转自:https://blog.csdn.net/weixin_43914604/article/details/105028759 学习课程:<2019王道考研计算机网络> 学习目的 ...

  3. 密码学基础:AES加密算法

    [原创]密码学基础:AES加密算法-密码应用-看雪论坛-安全社区|安全招聘|bbs.pediy.com 目录 基础部分概述: 第一节:AES算法简介 第二节:AES算法相关数学知识 素域简介 扩展域简 ...

  4. c 不同类型的指针

    今天看到了一个问题:c里面,不同类型的指针是否可以互指呢?也就是不同类型的指针之间是否可以互相赋值,我想了想,对于32位机子而言,所有类型的指针都是4Byte(64位就是8Byte,这里只讨论32位) ...

  5. 求树的直径【两遍BFS】

    两遍BFS.从任意一个点出发,第一遍可以找到直径的一端,从这端出发即可找到另外一端. 证明:从U点出发,到达V[画个图便清晰了] 1.如果U在直径上,则V一定是直径的一个端点. 2.如果U不在直径上. ...

  6. 通过silky框架在.net平台构建微服务应用

    目录 必要前提 使用Web主机构建微服务应用 使用.NET通用主机构建微服务应用 构建具有websocket服务能力的微服务应用 构建Silky微服务网关 开源地址 在线文档 在线示例 必要前提 (必 ...

  7. 在Delphi中高效执行JS代码

    因为一些原因,需要进行encodeURIComponent和decodeURIComponent编码,在Delphi中找了一个,首先是发现不能正确编码+号,后面强制处理替换了,勉强可用. 后面发现多次 ...

  8. QuantumTunnel:Netty实现

    接上一篇文章内网穿透服务设计挖的坑,本篇来聊一下内网穿透的实现. 为了方便理解,我们先统一定义使用到的名词: UserClient:用户客户端,真实的请求发起方: UserServer:内网穿透-用户 ...

  9. robotframework定位页面内Frame框架里的元素

    在自动化开发中,会遇到在页面内部点开一个新的页面后,新的页面元素无法定位到的情况,如点击下图的上传图片,弹出的界面无法直接定位到: 遇到这种情况需要查看弹出界面是否是Frame框架页面:打开火狐浏览器 ...

  10. 组件通信之全局事件总线 & 消息订阅发布

    全局事件总线 介绍 一种组件间通信的方式,适用于任意组件间通信. 在使用全局事件总线之前需要一些知识准备 所有组件实例的原型对象的原型对象就是 Vue 的原型对象,即VueComponent.prot ...