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的流程或者说技巧,大致上能够将其步骤简单的概括为:红灯->绿灯->重构。
可是测试是什么?测试是谁执行的?测试又是如何驱动开发的呢?下面咱们就经过一个小例子来聊一聊这个问题。
程序是什么?简单的说就是一段有预期输出的代码。咱们能够执行这段程序,并得到程序的输出。而所谓的测试,即是这样的一段程序,它会自动调用执行另外一段须要被测试的代码(在这里咱们依靠一些测试框架来实现,例如针对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的方法,所以咱们运行这个测试的结果即是失败。换言之,咱们如今处于红灯阶段。测试

绿灯

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

  1. 实现一个叫作HpComp的类
  2. 为HpComp增长一个字段currentHp,用来保存如今的血量
  3. 实现一个叫作TakeDamage的方法,而在这个测试中事实上只要求TakeDamage方法将currentHp的值变成50便可。

只要知足这3点,咱们就能够很轻易的使红灯变成绿灯。因此,为了知足测试条件,咱们能够十分简单粗暴的写出以下的代码:this

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方法编程了下面这个样子:3d

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

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

相关文章
相关标签/搜索