0x00 前言
关于TDD测试驱动开发的文章已经有很多了,但是在游戏开发尤其是使用Unity3D开发游戏时,却听不到特别多关于TDD的声音。那么本文就来简单聊一聊TDD如何在U3D项目中使用以及如何使用U3D 5.3.X之后版本已经集成的单元测试模块Editor Test Runner。
0x01 你好,TDD
TDD,测试驱动开发改变了我们常见的工作流程,不要求先写逻辑代码,反而要求先完成测试代码。待测试代码完成之后,我们再将目光转移到逻辑代码,根据测试的要求,完成逻辑代码,使之能够通过经过拆分后粒度已经很小的测试。这样做有什么好处呢?
- 要将任务拆分成可测试的各个测试用例,这就要求我们在完成逻辑代码时要将代码的功能尽可能细分,换句话说就是让一个类/方法只负责单一责任,当这个类/方法需要承担其他类型/方法的责任的时候,就需要分解这个类/方法。这就迫使我们要把程序设计成易于调用和可测试的,即迫使我们解除软件中的耦合。
- 更加适合应对需求的经常性变更。身处游戏开发行业的从业人员都不能否认的一点便是游戏开发中需求变更是一件不可避免甚至是必不可少的事情,而基于测试驱动开发的另一个好处便是一旦因为需求变更而出现bug,能够很快的发现,进而解决问题。
- 单元测试是一种无价的文档,它是展示方法或类如何使用的最佳文档。这份文档是可编译、可运行的,并且它保持最新,永远与代码同步。
0x02 流程,驱动
为了进行TDD测试驱动开发,我们需要了解TDD的流程或者说技巧,大体上可以将其步骤简单的归纳为:红灯->绿灯->重构。
但是测试是什么?测试是谁执行的?测试又是如何驱动开发的呢?下面我们就通过一个小例子来聊一聊这个问题。
程序是什么?简单的说就是一段有预期输出的代码。我们可以执行这段程序,并获得程序的输出。而所谓的测试,便是这样的一段程序,它会自动调用执行另一段需要被测试的代码(在这里我们依靠一些测试框架来实现,例如针对C#的测试框架NUnit),并且根据输出的可见结果来验证某些假设是否成立,例如输出的结果证明假设成立,则测试通过。
简单的了解了测试之后,我们通过一个小例子来看看测试驱动开发的思路和流程是怎样的,并且一探“驱动”的具体含义。
红灯
下面,我们就利用NUnit来编写我们的第一个测试,来看看测试是如何驱动开发的:
//测试被攻击之后伤害数值是否和预期值相等
[Test]
public void TakeDamage_BeAttacked_HpEqual()
{
HpComp health = new HpComp();
health.currentHp = 100;
health.TakeDamage(50);
Assert.AreEqual(50f, health.currentHp);
}
首先可以看到测试代码的方法名很长,而且测试名中还包括下划线来保证我们不会漏掉关于这个测试的重要信息(被测试的方法_测试进行的条件_预期结果),因为在编写测试代码时,可读性是重要的考量之一。
继续看测试代码,我们现在测试的类是HpComp,它包括一个字段currentHp保存了现在的血量值,还有一个方法TakeDamage。最开始我们会将currentHp初始化为100,之后调用TakeDamage方法,最后使用NUnit的Assert类所提供的静态方法AreEqual来断言假设是否成立,也即判断是否通过测试。
此时,由于我们还没有声明一个叫HpComp的类来处理和血量相关的逻辑,也没有一个叫currentHp的字段来保存现在的血量,更没有一个叫TakeDamage的方法,因此我们运行这个测试的结果便是失败。换言之,我们现在处于红灯阶段。
绿灯
测试写完了,此时是红灯,而此时将这个红灯变成绿灯的要求,便驱使着我们进行开发。所幸的是,我们要开发的内容,已经在测试中体现了出来:
- 实现一个叫做HpComp的类
- 为HpComp增加一个字段currentHp,用来保存现在的血量
- 实现一个叫做TakeDamage的方法,而在这个测试中事实上只要求TakeDamage方法将currentHp的值变成50即可。
只要满足这3点,我们就可以很轻易的使红灯变成绿灯。所以,为了满足测试条件,我们可以十分简单粗暴的写出如下的代码:
public class HpComp
{
public float currentHp;
public void TakeDamage(float damage)
{
this.currentHp = 50f;
}
}
好了,在上面的测试代码中只要调用TakeDamage方法,currentHp的值便被设置为了50,和断言中的预期符合,因此测试通过,状态也由红灯变成了绿灯。当然,我们简单的实现就通过了第一个测试,此时如果有优化代码的需求,我们就需要对代码进行重构,使得代码更加干净。
再来几次
我们的第一个测试用例驱动开发出的代码显然满足了第一个测试的需求,但是如果我们重新回到原点,并且思考一下除了满足第一个测试中提供的数据,我们的代码还能做什么,如果换一个测试条件结果会变得怎样呢?
我们来完成一个新的测试:
//测试被攻击之后伤害数值是否和预期值相等
[Test]
public void TakeDamage_BeAttacked_HpEqual2()
{
HpComp health = new HpComp();
health.currentHp = 150;
health.TakeDamage(10);
Assert.AreEqual(140f, health.currentHp);
}
这是一个新的测试(暂时叫做测试2),这就意味着TakeDamage方法除了通过第一个测试之外,还必须通过这个新的测试2。此时,我们最初的TakeDamage的实现,显然无法通过测试2,因此测试2是红灯状态。
这也就是说,随着我们的测试增加,会带来更多的预期和要求,从而驱动我们开发出满足这些预期和要求的代码来。随着测试2的出现,我们将TakeDamage方法编程了下面这个样子:
public void TakeDamage(float damage)
{
this.currentHp -= damage;
}
这样,它不仅通过了测试1,同时也通过了测试2。
但是如果我们重复上面的流程,提出更多的测试呢?也许我们还会发现TakeDamage方法可能会出现越界的情况,或者是输入不合法的情况等等。当然,这些都可以通过更多的测试来驱动我们开发出更健康的代码。
TDD流程小结
通过上面的小例子,我们可以看到TDD的流程或者说开发技巧并不难理解:
- 编写一个会失败的测试,以证明产品中的代码或功能的缺陷。
- 编写符合测试预期的代码。
- 重构代码,如果测试通过了,就可以选择重构,目标是使代码的可读性更强、减少重复代码。如果不重构,则可以开始编写下一个测试,即重复第4步。
- 重复以上过程。
0x03 问题,方案
由于游戏开发和传统软件开发之间的差异,因此在开发游戏的过程中编写单元测试,会面临两个主要的问题:
1.游戏开发中会涉及到很多的I/O操作处理,以及视觉和UI的处理,而这个部分是单元测试中比较难以处理的部分。
2.具体到使用Unity3D开发游戏,我们自然而然的希望能够将测试的框架集成到Unity3D的编辑器中,这样更加容易操作。
针对问题1,由于对I/O处理以及UI视觉方面的操作比较难以实施单元测试,所以我们单元测试的主要对象是逻辑操作以及数据存取的部分。
针对问题2,Unity5.3.x已经在editor中集成了测试模块。该测试模块依托了NUnit框架(NUnit是一个单元测试框架,专门针对于.NET来写的.其实在前面有JUnit(Java),CPPUnit(C++),他们都是xUnit的一员.最初,它是从JUnit而来.U3d使用的版本是2.6.4)。
而且除了Unity5.3.x自带的单元测试模块之外,Unity官方还推出了一款测试插件Unity Test Tool(基于NSubstitute)。
0x04 实践,U3D中的单元测试
在Untiy编辑器中写单元测试:
编写单元测试用例时,使用的主要是Unity Editor自带的单元测试模块,因此单元测试是基于NUnit框架的。
这就要求编写单元测试时,要引入NUnit.Framework命名空间,且单元测试类要加上[TestFixture]属性,单元测试方法要加上[Test]属性,并将测试用例的文件放在Editor文件夹下。
测试用例的编写结构要遵循3A原则,即Arrange, Act, Assert。
即先要设置测试环境,例如实例化测试类,为测试类的字段赋值。
之后操作对象,即写测试的行为。
最后是断言某件事情是预期的,即判断是否通过测试。
下面是一个例子:
using UnityEngine;
using System.Collections;
using NUnit.Framework;
[TestFixture]
public class HpCompTests
{
//测试被攻击之后伤害数值是否和预期值相等
[Test]
public void TakeDamage_BeAttacked_HpEqual()
{
HpComp health = new HpComp();
health.currentHp = 100;
health.TakeDamage(50);
Assert.AreEqual(50f, health.currentHp);
}
}
完成之后,我们就可以打开Unity 5.3.x中集成的单元测试模块来进行自动化测试了。
好了,本文到此就暂时打住了,之后有新的体验和想法,还会继续这个话题的总结,也欢迎各位讨论。
TDD在Unity3D游戏项目开发中的实践的更多相关文章
-
TDD 与 CI 在 Python 中的实践
社区化产品的长久生存之道可能莫过于对迭代周期的控制.还记得以前采用老土的阶段开发的年代,将软件生命周期分为各个阶段,当到达每个阶段的里程碑则集中所有的资源.人力作全面冲刺.每次到了里程碑的检查点冲过了 ...
-
Unity3d游戏开发中使用可空类型(Nullable Types)
你怎么确定一个Vector3,int,或float变量是否被分配了一个值?一个方便的方式就是使用可空类型! 有时变量携带重要信息,但仅仅有在特定的游戏事件发生时触发.比如:一个角色在你的游戏可能闲置, ...
-
[Unity3D]Unity3D游戏开发之跑酷游戏项目解说
大家好,我是秦元培.我參加了CSDN2014博客之星的评选,欢迎大家为我投票,同一时候希望在新的一年里大家能继续支持我的博客. 大家晚上好.我是秦元培,欢迎大家关注我的博客,我的博客地址是blog.c ...
-
【初码干货】使用阿里云对Web开发中的资源文件进行CDN加速的深入研究和实践
提示:阅读本文需提前了解的相关知识 1.阿里云(https://www.aliyun.com) 2.阿里云CDN(https://www.aliyun.com/product/cdn) 3.阿里云OS ...
-
Redis在WEB开发中的应用与实践
Redis在WEB开发中的应用与实践 一.Redis概述: Redis是一个功能强大.性能高效的开源数据结构服务器,Redis最典型的应用是NoSQL.但事实上Redis除了作为NoSQL数据库使用之 ...
-
[Unity3D]Unity3D游戏开发3D选择场景中的对象,并显示轮廓效果强化版
大家好,我是秦培,欢迎关注我的博客,我的博客地址blog.csdn.net/qinyuanpei. 在上一篇文章中,我们通过自己定义着色器实现了一个简单的在3D游戏中选取.显示物体轮廓的实例. 在文章 ...
-
【Unity3d游戏开发】Unity3D中常用的物理学公式
马三最近在一直负责Unity中的物理引擎这一块,众所周知,Unity内置了NVIDIA公司PhysX物理引擎.然而,马三一直觉得只会使用引擎而不去了解原理的程序猿不是一位老司机.所以对一些常用的物理学 ...
-
Unity3d Android Http 开发中的坑(吐槽
在一般的U3D网络开发中,直接使用WWW类便足够正常使用,但我在发现使用WWW下载大文件时,会导致整个程序卡顿的情况(不清楚是否我个人电脑问题),所以干脆使用HttpWebRequest/HttpWe ...
-
Unity3D游戏开发初探—3.初步了解U3D物理引擎
一.什么是物理引擎? 四个世纪前,物理学家牛顿发现了万有引力,并延伸出三大牛顿定理,为之后的物理学界的发展奠定了强大的理论基础.牛顿有句话是这么说的:“如果说我看得比较远的话,那是因为我站在巨人的肩膀 ...
随机推荐
-
JavaScript模板引擎artTemplate.js——引入子模板
之前的例子都是单一结构的对象,如果遇到复杂对象结构,我们可以通过引入子模板来实现html的渲染. 依旧以之前的数据作为例子: <div id="content">< ...
-
phpAdmin安装
phpAdmin是和Navicat重复的功能 负责管理MySql数据库 不过他是使用浏览器进行管理MySql数据库 PHP环境搭建的完整步骤 http://www.cnblogs.com/azhe-s ...
-
CSS中可以和不可以继承的属性
一.无继承性的属性 1.display:规定元素应该生成的框的类型 2.文本属性: vertical-align:垂直文本对齐 text-decoration:规定添加到文本的装饰 text-shad ...
-
jquery 上传图片即时预览功能
<script type="text/javascript"> jQuery.fn.extend({ uploadPreview: ...
-
Uva 10537 过路费
题目链接:http://vjudge.net/contest/143062#problem/C 题意: 给定一个无向图,大写字母是城市,小写字母是村庄,经过城市交过路费为当前货物的%5,路过村庄固定交 ...
-
python中给for循环增加索引
for index, item in enumerate(Foo()): print "index: ", index, " item: ",item 用enu ...
-
IOS uitableviewcell 向左滑动删除编辑等
主要实现这个方法就好了 -(NSArray<UITableViewRowAction *> *)tableView:(UITableView *)tableView editActions ...
-
node 控制 树莓派做的天气闹钟
node 控制 树莓派做的天气闹钟 在成都上班,下雨天堵车,迟到的概率会很大. 正好手上有一块树莓派 ,做了一个晴雨闹钟. 下雨天 早上 7:00叫我起床 晴天 早上 7:30叫我起床 将自己喜欢的歌 ...
-
剑指Offer——乐视笔试题+知识点总结
剑指Offer--乐视笔试题+知识点总结 情景回顾 时间:2016.9.19 15:10-17:10 地点:山东省网络环境智能计算技术重点实验室 事件:乐视笔试 总体来说,乐视笔试内容体量不算少, ...
-
最长公共前缀(java实现)
题目: 编写一个函数来查找字符串数组中的最长公共前缀. 如果不存在公共前缀,返回空字符串 "". 示例 1: 输入: ["flower","flow& ...