TDD如何在U3D项目中使用如何使用U3D 5.3.X之后版本已经集成的单元测试模块Editor Test Runner

2016年08月02日 15:56 0 点赞 0 评论 更新于 2017-05-09 14:00

关于TDD

TDD(测试驱动开发)改变了我们常见的工作流程。它不要求先编写逻辑代码,而是要求先完成测试代码。待测试代码完成后,再根据测试要求编写逻辑代码,使其能够通过经过拆分后粒度较小的测试。这种开发方式有以下好处:

降低代码耦合度

要将任务拆分成可测试的各个测试用例,这就要求我们在编写逻辑代码时,将代码功能尽可能细分,让一个类或方法只负责单一责任。当一个类或方法需要承担其他类型或方法的责任时,就需要对其进行分解。这迫使我们将程序设计成易于调用和可测试的,从而解除软件中的耦合。

更好应对需求变更

在游戏开发行业,需求变更是不可避免的。基于测试驱动开发的好处在于,一旦因需求变更出现bug,能够快速发现并解决问题。

提供有效文档

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

TDD流程

为了进行TDD测试驱动开发,我们需要了解其流程,大体可归纳为:红灯 -> 绿灯 -> 重构。下面通过一个小例子详细介绍这个流程。

红灯:编写失败的测试

测试本质上是一段程序,它会自动调用需要被测试的代码,并根据输出结果验证某些假设是否成立。若输出结果证明假设成立,则测试通过。我们使用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的方法,在这个测试中,只需要该方法将currentHp的值变为50即可。

为了满足测试条件,我们可以编写如下代码:

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);
}

最初实现的TakeDamage方法无法通过这个新测试,测试处于红灯状态。为了使代码通过新测试,我们将TakeDamage方法修改为:

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

修改后的方法不仅通过了第一个测试,也通过了新测试。随着测试的增加,我们可能会发现TakeDamage方法存在越界或输入不合法等问题,这些都可以通过更多的测试来驱动我们开发出更健壮的代码。

TDD流程小结

通过以上例子,TDD的流程可以总结为:

  1. 编写一个会失败的测试,以证明产品中的代码或功能存在缺陷。
  2. 编写符合测试预期的代码。
  3. 重构代码,如果测试通过,可以选择重构代码,目标是提高代码的可读性、减少重复代码。如果不重构,则开始编写下一个测试。
  4. 重复以上过程。

游戏开发中TDD面临的问题及解决方案

由于游戏开发和传统软件开发存在差异,在游戏开发过程中编写单元测试会面临两个主要问题:

难以处理的部分

游戏开发涉及大量的I/O操作处理,以及视觉和UI处理,这些部分在单元测试中较难处理。解决方案是将单元测试的主要对象聚焦于逻辑操作和数据存取部分。

集成测试框架

在使用Unity3D开发游戏时,我们希望将测试框架集成到Unity3D编辑器中,以便于操作。Unity 5.3.x已经在编辑器中集成了测试模块,该模块依托NUnit框架(NUnit是专门针对.NET的单元测试框架,U3D使用的版本是2.6.4)。此外,Unity官方还推出了一款基于NSubstitute的测试插件Unity Test Tool。

在U3D中进行单元测试的实践

在Unity编辑器中编写单元测试时,主要使用Unity Editor自带的单元测试模块,基于NUnit框架。编写单元测试需要遵循以下步骤:

  1. 引入NUnit.Framework命名空间。
  2. 为单元测试类添加[TestFixture]属性。
  3. 为单元测试方法添加[Test]属性。
  4. 将测试用例文件放在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中集成的单元测试模块进行自动化测试。

好了,本文到此结束,之后有新的体验和想法,会继续总结这个话题,也欢迎各位参与讨论。

作者信息

孟子菇凉

孟子菇凉

共发布了 1189 篇文章