您的位置:软件测试 > 开源软件测试 > 开源单元测试工具 > Nunit
使用NUnit为游戏项目编写高质量单元测试的思考
作者:网络转载 发布时间:[ 2016/7/21 13:54:19 ] 推荐标签:单元测试 NUnit

  0x00 单元测试Pro & Con
  近尝试在我参与的游戏项目中引入TDD(测试驱动开发)的开发模式,因此单元测试便变得十分必要。这篇博客来聊一聊这段时间的感悟和想法。由于游戏开发和传统软件开发之间的差异,因此在开发游戏,特别是使用Unity3D开发游戏的过程中编写单元测试往往会面临两个主要的问题:
  游戏开发中会涉及到很多的I/O操作处理,以及视觉和UI的处理,而这个部分是单元测试中比较难以处理的部分。
  具体到使用Unity3D开发游戏,我们自然而然的希望能够将测试的框架集成到Unity3D的编辑器中,这样更加容易操作。
  但是,单元测试的好处也十分多。
  TDD,测试驱动开发。编写单元测试将使我们从调用者观察、思考。特别是先写测试,迫使我们把程序设计成易于调用和可测试的,即迫使我们解除软件中的耦合。可以将任务的粒度降低。当然TDD是否适合游戏开发尚有争论,但是单元测试的必要性是无需置疑的。
  单元测试是一种无价的文档,它是展示方法或类如何使用的佳文档。这份文档是可编译、可运行的,并且它保持新,永远与代码同步。
  更加适合应对需求的经常性变更。身处游戏开发行业的从业人员都不能否认的一点便是游戏开发中需求变更是一件不可避免甚至是必不可少的事情,而单元测试另一个好处便是一旦因为需求变更而出现bug,能够很快的发现,进而解决问题。
  0x01 Unity3D中常用的测试工具
  针对问题1,由于对I/O处理以及UI视觉方面的操作比较难以实施单元测试,所以我们单元测试的主要对象是逻辑操作以及数据存取的部分。
  针对问题2,Unity5.3.x已经在editor中集成了测试模块。该测试模块依托了NUnit框架(NUnit是一个单元测试框架,专门针对于.NET来写的.其实在前面有JUnit(Java),CPPUnit(C++),他们都是xUnit的一员.初,它是从JUnit而来.U3d使用的版本是2.6.4)。
  在Unity Editor中实现测试而不是在IDE中进行测试的原因在于,一些Unity的API需要在Unity的环境中来运行,而无法直接在外部的IDE中实现,例如实例化GameObject。
  而且除了Unity5.3.x自带的单元测试模块之外,Unity官方还推出了一款测试插件Unity Test Tool(基于NSubstitute),除了单元测试之外还包括:
  · 单元测试
  · 集成测试
  · 断言组件
  需要指出的是Unity Test Tool基于NSubstitute这个库。
  0x02 初识单元测试
  既然本文的主题是单元测试,那么我们必须先对单元测试下一个定义:
  一个单元测试是一段自动化的代码,这段代码调用被测试的工作单元,之后对这个单元的单个终结果的某些假设进行检验。单元测试使用单元测试框架编写,并要求单元测试可靠、可读并且可维护。只要产品代码不发生变化,单元测试的结果是稳定的。
  既然有了单元测试的定义,下面我们尝试在Unity项目中写单元测试吧。
  一个单元测试的小例子:
  编写单元测试用例时,使用的主要是Unity Editor自带的单元测试模块,因此单元测试是基于NUnit框架的。
  借助NUnit,我们可以:
  编写结构化的测试。
  自动执行选中的或全部的单元测试。
  查看测试运行的结果。
  因此这要求编写Unity3D项目的单元测试时,要引入NUnit.Framework命名空间,且单元测试类要加上[TestFixture]属性,单元测试方法要加上[Test]属性,并将测试用例的文件放在Editor文件夹下。
  下面是一个例子:
using UnityEngine;
using System.Collections;
using NUnit.Framework;
[TestFixture]
public class HpCompTests
{
//测试被攻击之后伤害数值是否和预期值相等
[Test]
public void TakeDamage_BeAttacked_HpEqual()
{
//Arrange
HpComp health = new HpComp();
health.currentHp = 100;
//Act
health.TakeDamage(50);
//Assert
Assert.AreEqual(50f, health.currentHp);
}
}
  该例子是测试英雄受到伤害之后,血量是否和预期的相等。
  测试框架会创建这个测试用例类,并且调用TakeDamage_BeAttacked_HpEqual方法来和其交互,后使用Nunit的Assert类来断言是否通过测试。
  0x03 单元测试的结构
  通过上面的小例子,我们可以发现单元测试其实是有结构的。下面我们来具体分析一下:
  使用NUnit提供的特性来标识测试代码
  NUnit使用C#的特性机制识别和加载测试。这些特性像是书签,用来帮助测试框架识别哪些部分是需要调用的测试。
  如果要使用NUnit的特性,我们需要在测试代码中首先引入NUnit.Framework命名空间。
  而NUnit运行器至少需要两个特性才知道需要运行什么。
  [TestFixture]:标识一个自动化NUnit测试的类。
  [Test]:可以加在一个方法上,标识这个方法是一个需要调用的自动化测试。
  当然,还有一些别的特性供我们使用,来方便我们更好的控制测试代码,例如[Category]特性可以将测试分类、[Ignore]特性可以忽略测试。
  常用的NUnit属性见下表:
  [SetUp]
  [TearDown]
  [TestFixture]
  [Test]
  [TestCase]
  [Category]
  [Ignore]
  测试命名和布局标准
  测试类的命名:
  对应被测试项目中的一个类,创建一个名为[ClassName]Tests的类。
  工作单元的命名:
  对每个工作单元(测试),测试方法的方法名由三部分组成,并且按照如下规则命名:[被测试的方法名]_[测试进行的假设条件]_[对测试方法的预期]。
  具体来说:
  被测试的方法名
  测试进行的假设条件,例如“登入失败”、“无效用户”、“密码正确”。
  对测试方法的预期:在测试场景指定的条件下,我们对被测试方法的行为的预期。
  其中,对测试方法的预期会有三种可能的结果:
  返回一个值(数值、布尔值等等)。
  改变被测试的系统的一个状态。
  调用一个第三方系统。
  可以看出,我们的测试代码在格式上与标准的代码有所不同,测试名可以很长,但是在编写测试代码时,可读性是为重要的方面之一,而测试名中的下划线可以令我们不会遗漏所有的重要信息,我们甚至可以将测试方法名当做一个句子来读,这样会使得这个测试方法的测试目标、场景以及预期都十分明确,无需额外的注释。
  测试单元的行为——3A原则
  有了NUnit属性可以标识可以自动运行的测试代码和测试代码的一些命名规则,下面我们来看看如何测试自己的代码。
  一个单元测试通常包含三个行为,可以归纳为3A原则即:
  Arrange,准备对象,创建对象并进行必要的设置。
  Act,操作对象。
  Assert,断言某件事情是预期的。
  下面是之前的那段简单的代码,包含了以上的NUnit的属性、命名规范以及3A原则下的行为,其中断言部分使用了NUnit框架提供的Assert类,被测试的类为HpComp,被测试的方法为
TakeDamage。
using NUnit.Framework;
[TestFixture]
public class HpCompTests
{
//测试被攻击之后伤害数值是否和预期值相等
[Test]
public void TakeDamage_BeAttacked_HpEqual()
{
//Arrange
HpComp health = new HpComp();
health.currentHp = 100;
//Act
health.TakeDamage(50);
//Assert
Assert.AreEqual(50f, health.currentHp);
}
}
  单元测试的断言——Assert类
  NUnit框架提供了一个Assert类来处理断言的相关功能。Asset类用于声明某个特定的假设应该成立,因此如果传递给Assert类的参数和我们断言(预期)的值不同,则NUnit框架会认为测试没有通过。
  Assert类会提供一些静态方法,供我们使用。
  例如:
  Assert.AreEqual(预期值,实际值);
  Assert.AreEqual(1,2 - 1);
  关于Assert类的静态方法,各位可以直接在代码中看。

上一页12下一页
软件测试工具 | 联系我们 | 投诉建议 | 诚聘英才 | 申请使用列表 | 网站地图
沪ICP备07036474 2003-2017 版权所有 上海泽众软件科技有限公司 Shanghai ZeZhong Software Co.,Ltd