TA的每日心情 | 无聊 2024-9-27 10:07 |
---|
签到天数: 62 天 连续签到: 1 天 [LV.6]测试旅长
|
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(基于NSu
bstitute),除了单元测试之外还包括:
单元测试
集成测试
断言组件
需要指出的是Unity Test Tool基于NSubstitute这个库。
0x02 初识单元测试
既然本文的主题是单元测试,那么我们就必须先对单元测试下一个定义:
一个单元测试是一段自动化的代码,这段代码调用被测试的工作单元,之后对这个单元的单个最终结果的
某些假设进行检验。单元测试使用单元测试框架编写,并要求单元测试可靠、可读并且可维护。只要产品
代码不发生变化,单元测试的结果是稳定的。
既然有了单元测试的定义,下面我们就尝试在Unity项目中写单元测试吧。
一个单元测试的小例子:
编写单元测试用例时,使用的主要是Unity Editor自带的单元测试模块,因此单元测试是基于NUnit框架的。
借助NUnit,我们可以:
编写结构化的测试。
自动执行选中的或全部的单元测试。
查看测试运行的结果。
因此这就要求编写Unity3D项目的单元测试时,要引入NUnit.Framework命名空间,且单元测试类要加上[Te
stFixture]属性,单元测试方法要加上[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类的静态方法,各位可以直接在代码中看。
0x04 单元测试的可靠性
我们的目标是写出可靠、可维护、可读的测试。
因此,除了遵循单元测试结构规范编写单元测试之外,我们还需要注意可靠性、可维护性以及可读性这
些方面。因此,一些原则我们也需要注意。
不轻易删除和修改测试
一旦测试写好了并且通过了,就不应该轻易的修改和删除这些测试。因为这些测试是对应系统代码的保
护伞,在修改系统代码时,这些测试会告诉我们修改后的代码是否会破坏已有的功能。
尽量避免测试中的逻辑
随着测试中的逻辑增多,测试代码出现缺陷的几率也会增大。而且由于我们往往相信测试是可靠的,因
此一旦测试出现缺陷我们往往不会首先考虑是测试的问题,可能会浪费时间去修改系统代码。而单元测试
中,最好保持逻辑的简单,因此尽量避免使用下面的逻辑控制代码。
switch、if
foreach、for、while
一个单元测试应该是一系列的方法调用和断言,但是不应该包含控制流语句。
只测试一个关注点
在一个单元测试中验证多个关注点会使得测试代码变得复杂,但却没有价值。相反,我们应该在分开的、
独立的单元中验证多余的关注点,这样才能发现真正导致失败的地方。
0x05 单元测试的可维护性
去除重复代码
和系统中的重复代码一样,在单元测试中重复代码同样意味着测试对象某方面改变时要修改更多的测试代码。
如果测试看上去都一样,仅仅是参数不同,那么我们完全可以使用参数化测试即使用[TestCase]特性将不同
的数据作为参数传入测试方法。
实施测试隔离
所谓的测试隔离,指的是一个测试和其他的测试隔离,甚至不知道其他测试的存在,而只在自己的小世界中运行。
将测试隔离的目的是防止测试之间的互相影响,常见的测试之间互相影响的情况可以总结如下:
强制的测试顺序:测试要以某种顺序执行,后一个测试需要前面的测试结果,这种情况有可能会导致问题的
原因是因为NUnit不能保证测试按照某种特定的顺序执行,因此今天通过的测试,明天可能就不好用了
隐藏的测试调用:测试调用其他测试
共享状态被破坏:测试要共享状态,但是在一个测试完成之后没有重置状态,进而影响后面的测试
0x06 单元测试的可读性
正如概述中所说单元测试是一种无价的文档,它是展示方法或类如何使用的最佳文档。因此,可读性这条要
求的重要性便可见一斑。试想一下即便是几个月之后别的程序员都可以通过单元测试来理解一个系统的组成
以及使用方法,并能够很快的理解他们要做的工作以及在哪里切入。
单元测试命名
在单元测试的结构中已经有过要求和介绍。参考那部分。
单元测试中的变量命名
|
|