介绍

在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。

单元测试 - 维基百科,自由的百科全书

下面介绍 Unity 中的单元测试。

Unity API 调用错误

在 Visual Studio 这种脱离 Unity 的环境中调用 Unity 的 API 时会报异常:

1
2
  Expected: No Exception to be thrown
  But was:  <System.Security.SecurityException: ECall 方法必须打包到系统模块中。

猜测可能是因为这些 API 都是 Native 代码的 Wrapper,所以无法调用。也就是说,在进行单元测试时,受环境限制,无法对包含 Unity API 的代码进行测试。这个问题直接导致了 Unity 单元测试发展分成了两个阶段。

不调用 Unity API 的阶段

这一阶段所有的官方博客文章都是 2014 年写的,介绍的内容都是如何将代码与 Unity API 剥离,然后如何测试,可以说是非常正统的单元测试方法。

只是测试的时候将所有 Unity API 剥离的话,需要对项目的结构进行清晰地划分,必须实现逻辑与 IO 分离(此处的 IO 泛指输入与输出,例如渲染、声音、键盘鼠标输入等等),逻辑与 IO 分离一些可行的做法包括:逻辑与渲染分离、MVC 架构、事件系统等等。

这个阶段实际上假定 Unity API 并不是要测试代码的一部分,而是算作外部代码,即不受控的代码。如果需要测试 Unity API,这个阶段可用的方案是使用集成测试:

而且官方后续的态度也很奇怪,直接在 Unity 5.6 时废弃了 UnityTestTools 这个集成测试框架,然后在 Unity 中集成了一个单元测试框架,可问题是这两个东西根本不冲突且可以并存。

调用 Unity API 的阶段

Unity 魔改了 NUnit 这个框架,让其支持在 Unity 中运行 Unity API,但是依然无法做到在 Unity 外运行 Unity API。

如何使用官方方案,这里有一些不错的例子:

实践

在编写一个框架时尝试引入单元测试,在横向比较了多款可以运行单元测试的框架后,NCrunch 成为胜出者。

NCrunch 最大的亮点就是可以在编写过程中实时地运行单元测试并报告结果,无需手动保存或显式运行单元测试。打开官网观看首页的视频即可了解这个强大的功能。

在设计框架时就将集成单元测试作为目标之一,由于选择了 NCrunch,导致的直接结果就是在单元测试中不可以调用 Unity API,因此最终的方案是修改设计,完全通过接口与 Unity API 进行交互。

按照正统的单元测试方法,所有与外部的交互通过接口,并且实现了两套接口代码:一套用于单元测试、另一套用于与 Unity 集成。

Unity 生成的 Visual Studio 工程中自动引用了 Unity 内部集成的 NSubstitute DLL,这个内部 NSubstitute DLL 版本实际是魔改版本,无法与 NCrunch 结合使用。

因此编写了一个简单的小插件去替换内部的 NSubstitute DLL 引用为外部的 NSubstitute DLL 引用:NCrunch Adapter For Unity - 狂飙

由于集成了单元测试,所有的交互都通过接口隔离了,整个框架看起来较难理解。如果想要查看接口对应的实现,必须使用支持查看接口实现定义的 IDE。

正确的做法

正确的做法是从更高的层面看这个问题,这里引用《程序员修炼之道(第2版)》中的一段文字:

即使你碰巧命中了代码的每一行,也不代表全部。重要的是程序可能具有的状态数。状态和代码行并不等价。例如,假设有一个函数,它接受两个整数,每个整数可以是 0 到 999 之间的数字:
int test(int a, int b) {
    return a/ (a + b);
}
理论上,这个三行的函数有 1,000,000 个逻辑状态,其中 999,999 个可以正常工作,而另一个则不能(当 a + b 等于零时)。仅仅知道执行了这行代码并不能告诉你这些——你需要识别出程序的所有可能状态。不幸的是,通常这真是一个大难题——难到“还没等你解决了这个问题,太阳已经变成一个冰冷的硬疙瘩”。

提示93 测试状态覆盖率,而非代码覆盖率
P285

有更有效的方法吗?

使用《程序员修炼之道(第2版)》介绍的契约式编程可以定义什么条件是对的,符合条件的一定可以给出正确结果。不符合条件的直接报错,可以快速发现问题。

《The Science of Programming》讲解了如何去证明。

总结

单元测试

优点:

  • 当发现错误状态的组合时,通常会编写对应的单元测试以防止代码再次被这个错误状态的组合破坏。
  • 可以与持续集成/持续交付系统结合使用,实现提交推送后检查是否有修改代码导致的回归 Bug。

缺点:

  • 难以集成到现有系统中,需要做较大范围的改造,难度极大;最好是在项目开始前作出规划。
  • 开发人员需要培训,理解单元测试及其优缺点,明确使用范围。

参考资料