TDD与Unity3D游戏开发

0x00 前言

关于TDD测试驱动开发的文章已经有很多了,但是在游戏开发尤其是使用Unity3D开发游戏时,却听不到特别多关于TDD的声音。那么本文就来简单聊一聊TDD如何在U3D项目中使用以及如何使用U3D 5.3.X之后版本已经集成的单元测试模块Editor Test Runner。

0x01 你好,TDD

TDD,测试驱动开发改变了我们常见的工作流程,不要求先写逻辑代码,反而要求先完成测试代码。待测试代码完成之后,我们再将目光转移到逻辑代码,根据测试的要求,完成逻辑代码,使之能够通过经过拆分后粒度已经很小的测试。这样做有什么好处呢?

  1. 要将任务拆分成可测试的各个测试用例,这就要求我们在完成逻辑代码时要将代码的功能尽可能细分,换句话说就是让一个类/方法只负责单一责任,当这个类/方法需要承担其他类型/方法的责任的时候,就需要分解这个类/方法。这就迫使我们要把程序设计成易于调用和可测试的,即迫使我们解除软件中的耦合。

  2. 更加适合应对需求的经常性变更。身处游戏开发行业的从业人员都不能否认的一点便是游戏开发中需求变更是一件不可避免甚至是必不可少的事情,而基于测试驱动开发的另一个好处便是一旦因为需求变更而出现bug,能够很快的发现,进而解决问题。

  3. 单元测试是一种无价的文档,它是展示方法或类如何使用的最佳文档。这份文档是可编译、可运行的,并且它保持最新,永远与代码同步。

0x02 流程,驱动

为了进行TDD测试驱动开发,我们需要了解TDD的流程或者说技巧,大体上可以将其步骤简单的归纳为:<font color=red>红灯</font>-><font color=green>绿灯</font>->重构。
但是测试是什么?测试是谁执行的?测试又是如何驱动开发的呢?下面我们就通过一个小例子来聊一聊这个问题。
程序是什么?简单的说就是一段有预期输出的代码。我们可以执行这段程序,并获得程序的输出。而所谓的测试,便是这样的一段程序,它会自动调用执行另一段需要被测试的代码(在这里我们依靠一些测试框架来实现,例如针对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的方法,因此我们运行这个测试的结果便是失败。换言之,我们现在处于<font color=red>红灯</font>阶段。

绿灯

测试写完了,此时是红灯,而此时将这个红灯变成绿灯的要求,便驱使着我们进行开发。所幸的是,我们要开发的内容,已经在测试中体现了出来:

  1. 实现一个叫做HpComp的类

  2. 为HpComp增加一个字段currentHp,用来保存现在的血量

  3. 实现一个叫做TakeDamage的方法,而在这个测试中事实上只要求TakeDamage方法将currentHp的值变成50即可。

只要满足这3点,我们就可以很轻易的使红灯变成绿灯。所以,为了满足测试条件,我们可以十分简单粗暴的写出如下的代码:

public class HpComp
{
  public float currentHp;

  public void TakeDamage(float damage)
    {
      this.currentHp = 50f;
    }
}

好了,在上面的测试代码中只要调用TakeDamage方法,currentHp的值便被设置为了50,和断言中的预期符合,因此测试通过,状态也由<font color=red>红灯</font>变成了<font color=green>绿灯</font>。当然,我们简单的实现就通过了第一个测试,此时如果有优化代码的需求,我们就需要对代码进行重构,使得代码更加干净。

再来几次

我们的第一个测试用例驱动开发出的代码显然满足了第一个测试的需求,但是如果我们重新回到原点,并且思考一下除了满足第一个测试中提供的数据,我们的代码还能做什么,如果换一个测试条件结果会变得怎样呢?
我们来完成一个新的测试:

//测试被攻击之后伤害数值是否和预期值相等
  [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是<font color=red>红灯</font>状态。
这也就是说,随着我们的测试增加,会带来更多的预期和要求,从而驱动我们开发出满足这些预期和要求的代码来。随着测试2的出现,我们将TakeDamage方法编程了下面这个样子:

public void TakeDamage(float damage)
    {
      this.currentHp -= damage;
    }

这样,它不仅通过了测试1,同时也通过了测试2。
但是如果我们重复上面的流程,提出更多的测试呢?也许我们还会发现TakeDamage方法可能会出现越界的情况,或者是输入不合法的情况等等。当然,这些都可以通过更多的测试来驱动我们开发出更健康的代码。

TDD流程小结

通过上面的小例子,我们可以看到TDD的流程或者说开发技巧并不难理解:

  1. 编写一个会失败的测试,以证明产品中的代码或功能的缺陷。

  2. 编写符合测试预期的代码。

  3. 重构代码,如果测试通过了,就可以选择重构,目标是使代码的可读性更强、减少重复代码。如果不重构,则可以开始编写下一个测试,即重复第4步。

  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中集成的单元测试模块来进行自动化测试了。

好了,本文到此就暂时打住了,之后有新的体验和想法,还会继续这个话题的总结,也欢迎各位讨论。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


这篇文章将为大家详细讲解有关Unity3D中如何通过Animator动画状态机获取任意animation clip的准确播放持续时长,小编觉得挺实用的,因此分享给大家做个参考,
这篇文章主要介绍了Unity3D如何播放游戏视频,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解
这篇文章给大家分享的是有关Unity3D各平台路径是什么的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。1、Resources路径 Reso...
小编给大家分享一下Unity3D如何实现移动平台上的角色阴影,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧!由于目前主流使用Unity3.x在移动平...
如何解析基于Unity3D的平坦四叉树地形与Virtual Texture的分析,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希
这篇文章主要介绍Unity3D如何实现动态分辨率降低渲染开销,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!之前项目降低分辨率我们都普...
这篇文章主要介绍了unity3d中如何使用屏幕空间改善shadowmap漏光,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编...
这篇文章主要介绍unity3d如何实现基于屏幕空间的描边,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!Outline(Based on Image Space)由...
这篇文章给大家分享的是有关unity3d中导入fbx时的Scale是什么的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。在Unity中点击GameOb...
这篇文章主要为大家展示了“unity3d中如何实现ttc转ttf及制作字体”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习
这篇文章主要介绍了unity3d中水彩风渲染有什么用,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了...
这篇文章将为大家详细讲解有关unity3d中图像压缩原理是什么,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。1 图像可压缩...
这篇文章给大家分享的是有关unity3d中光照公式有哪些的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。漫反射、高光、物理渲染(PBR...
小编给大家分享一下unity3d中光照探针的示例分析,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我...
这篇文章将为大家详细讲解有关Unity3D中Rendering Paths及LightMode的示例分析,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有
这篇文章将为大家详细讲解有关unity3d中图形学的光照原理是什么,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。首先,在...
这篇文章给大家分享的是有关unity3d中图片渲染流程是什么的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。相关名词GPU(Graphic Pr...
本篇我们来介绍一下左侧工具栏中基本绘制的应用。 一、墙体绘制直墙 & 矩形墙绘制墙体时,可以看到上方的工具栏中对墙体进行参数的设定。 弧形墙在建筑版的户...
xlua是由腾讯维护的一个开源项目,我们可以在github上下载这个开源项目并查看一些相关文档官网:https://github.com/Tencent/xLua配置文档:https://github.com/Tencent/xLua/blob/master/Assets/XLua/Doc/hotfix.md常见问题解答:https://github.com/Tencent/xLua/blob/master/Assets/
我们都知道,一个三维场景的画面的好坏,百分之四十取决于模型,百分之六十取决于贴图,可见贴图在画面中所占的重要性。在这里我将列举一些贴图,并且初步阐述其概念,理解原理的基础上制作贴图,也就顺手多了。我在这里主要列举几种UNITY3D中常用的贴图,与大家分享,希望对大家有帮助。01 首先