TDD如何在U3D项目中使用如何使用U3D 5.3.X之后版本已经集成的单元测试模块Editor Test Runner
关于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的流程可以总结为:
- 编写一个会失败的测试,以证明产品中的代码或功能存在缺陷。
- 编写符合测试预期的代码。
- 重构代码,如果测试通过,可以选择重构代码,目标是提高代码的可读性、减少重复代码。如果不重构,则开始编写下一个测试。
- 重复以上过程。
游戏开发中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框架。编写单元测试需要遵循以下步骤:
- 引入
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中集成的单元测试模块进行自动化测试。
好了,本文到此结束,之后有新的体验和想法,会继续总结这个话题,也欢迎各位参与讨论。