IL2CPP 内部:测试框架

JOSH PETERSON / UNITY TECHNOLOGIESSenior Software Engineer
Jul 20, 2015|9 Min
IL2CPP 内部:测试框架
为方便起见,此网页已进行机器翻译。我们无法保证翻译内容的准确性或可靠性。如果您对翻译内容的准确性有疑问,请参阅此网页的官方英文版本。
这是 IL2CPP Internals 系列的第八篇,也是最后一篇。在本篇文章中,我将稍稍偏离前几篇文章的内容,不再讨论 IL2CPP 在编译时或运行时如何工作的某些方面。接下来,我们将简要介绍如何开发和测试 IL2CPP。
测试优先的开发

IL2CPP 团队具有强烈的测试优先开发思想。IL2CPP 的大部分代码都是采用测试驱动开发(TDD) 实践编写的,很少有未经大量测试覆盖的拉取请求被合并到 IL2CPP 代码中。

由于 IL2CPP 的输入集合有限(尽管相当大),即ECMA 335规范,因此其开发过程非常符合 TDD 概念。大多数测试都是在编写生产代码之前进行的,而这些测试总是需要在编写代码使其通过之前以预期的方式失败。

这一过程有助于推动 IL2CPP 的设计,同时也为开发团队提供了大量的测试库,这些测试库运行速度相当快,几乎可以测试 IL2CPP 中的所有现有行为。作为开发团队,这个测试套件有两个重要的好处。

1) 自信:在 IL2CPP 中,大多数重构代码的更改都能以很高的可信度完成。如果测试通过,则出现回归的可能性很小。

2) 故障排除:由于 IL2CPP 中的代码会按照我们的预期运行,因此错误几乎总是代码中未实现的部分或我们尚未考虑的情况。通过这种方式缩小特定错误的可能原因范围,我们可以更快地纠正错误。

测试统计

我们针对 IL2CPP 代码库运行的各类测试可分为几个不同的级别。以下是我们目前每个级别的测试数量(我将在下文讨论每种测试的具体内容)。

  • 单元测试
  • C#:472
  • C++:44
  • 集成测试
  • C#:1735
  • IL:173

如果所有这些测试都是绿色的,那么我们就有信心在那一刻交付 IL2CPP。我们为 IL2CPP 维护一个主要开发分支,它始终跟踪整个 Unity 开发的前沿分支。在这个主开发分支上,测试始终是绿色的。如果它们坏了(偶尔会发生),通常几分钟内就会有人修好。

由于我们团队的开发人员经常分叉这个主分支用于个人开发,因此它需要始终保持绿色。主开发分支和个人分支的构建和测试状态都由 Unity 的内部构建管理系统Katana 维护。

我们使用NUnit运行所有这些测试,并以三种不同方式之一驱动 NUnit

  • 视窗ReSharper
  • OSX:Xamarin Studio
  • 在 Windows 和 OSX 系统的构建机器上使用命令行:自定义 Perl 脚本

测试类型

我在上文提到了四种不同类型的测试,但没有做太多解释。每种类型的测试都有不同的目的,它们共同帮助 IL2CPP 开发向前发展。

单元测试验证一小段代码(通常是一个方法)的行为。它们设置一个情境,执行被测代码,最后断言一些预期行为。

IL2CPP 的集成测试实际上是在程序集上运行 il2cpp.exe 工具,将生成的 C++ 代码编译为可执行文件,然后运行可执行文件。由于我们对 IL2CPP 行为(Unity 中使用的现有 Mono 版本)有很好的参考,因此这些集成测试也可以使用 Mono(以及 Windows 上的 .Net)运行相同的程序集。然后,我们的测试运行程序会比较转储到标准输出的两个(或三个)运行结果,并报告任何差异。因此,IL2CPP 集成测试不像单元测试那样在测试代码中列出明确的预期值或断言。

C# 单元测试

这些测试是我们编写的速度最快、级别最低的测试。它们用于验证 IL2CPP 的 AOT 编译器实用程序 il2cpp.exe 中许多部分的行为。由于 il2cpp.exe 完全是用 C# 编写的,因此我们可以使用快速的 C# 单元测试来获得良好的更改周转时间。所有的 C# 单元测试都能在良好的开发机器上几秒钟内完成。

C++ 单元测试

IL2CPP 的绝大部分运行时代码(称为 libil2cpp)都是用 C++ 编写的。对于不易从公共应用程序接口访问的代码部分,我们使用 C++ 单元测试。由于 libil2cpp 中的大部分代码行为都可以通过更大的集成测试套件进行测试,因此我们的此类测试相对较少。这些测试比单元测试需要更多的时间,因为它们需要运行 il2cpp.exe 来设置夹具数据。

C# 集成测试

IL2CPP 最大、最全面的测试套件是 C# 集成测试套件。这些测试又分为几个小部分,重点是验证 icalls 行为、代码生成、p/invoke 和一般行为的测试。该套件中的大多数测试都相当简短,大约只有 5 至 10 行。整个套件在大多数机器上运行不到一分钟,但我们可以使用与剥离和代码生成相关的各种 IL2CPP 选项来运行它。

IL 集成测试

这些测试的工具链与 C# 集成测试类似。不过,我们不用 C# 编写测试代码,而是使用ILGenerator类直接创建程序集。虽然编写这些测试比 C# 测试要花费更多时间,但它们提供了更大的灵活性。我们经常会遇到当前 Mono C# 编译器生成的 IL 代码无效或未生成的问题。在这种情况下,我们通常可以用 IL 代码编写一个好的测试用例。这些测试还有助于全面测试conv.i(及其家族中的类似操作码)等操作码,这些操作码的行为清晰,但有许多细微变化。所有 IL 测试从头到尾不到一分钟即可完成。

我们在卡塔娜上通过多种变体和选项进行所有这些测试。从拉取源代码到完成测试运行,我们可以看到大约 20-30 分钟的运行时间,具体取决于构建农场的负载情况。

为什么要进行这么多集成测试?

根据这些描述,我们对 IL2CPP 的测试金字塔似乎是颠倒的。事实上,端到端集成测试(靠近金字塔顶端)占了我们测试覆盖范围的大部分。

在测试时间超过几秒的情况下进行 TDD 实践也很困难。我们通过允许运行集成测试套件的各个部分,以及对测试套件中生成的 C++ 代码进行增量构建,来缓解这一问题(这就是我们利用 IL2CPP 为 Unity 项目验证增量构建可能性的方式,敬请关注)。因此,单个测试的周转时间是合理的(尽管仍然没有我们希望的那么快)。

不过,大量使用集成测试是一个有意识的决定。IL2CPP 中的许多代码看起来都与以往不同,即使是在 2015 年 1 月首次公开发布时也是如此。自 IL2CPP 代码库建立以来,我们学到了很多东西,也修改了很多实现细节,但我们仍然保留着很多年前编写的原始测试。在尝试了多个不同层次的测试(甚至包括验证生成的 C++ 源代码的内容)后,我们认为这些集成测试为我们提供了最佳的运行时间与测试稳定性比。当 IL2CPP 代码发生变化时,我们很少需要修改现有的集成测试。这一事实给了我们极大的信心,让我们相信导致测试失败的代码更改确实是一个问题。它还能让我们毫无顾虑地重构和改进 IL2CPP 代码。

更大规模的测试

除了 IL2CPP 本身,IL2CPP 代码还融入了更大的 Unity 测试生态系统。对于支持 IL2CPP 的每个平台,我们都会执行 Unity 播放器运行测试。这些测试构建了一个包含 1000 多个场景的 Unity 项目,然后执行每个场景并通过断言验证预期行为。对于 IL2CPP 的更改,我们通常不会在该套件中添加新的测试(这些测试通常最终会在更低的级别上进行)。该套件可用于检查我们在特定平台上使用 IL2CPP 时可能出现的问题。该套件还允许我们测试将 IL2CPP 集成到 Unity 构建工具链中所使用的代码,每个平台的代码也各不相同。一个典型的运行测试套件大约需要 60-90 分钟才能完成,不过我们在本地执行单个测试的速度往往要快得多。

我们用于 IL2CPP 的最大、最慢的测试是 Unity 编辑器集成测试。实际上,每个测试都运行一个不同的 Unity 编辑器实例。大多数 IL2CPP 编辑器集成测试都侧重于构建和运行一个项目,通常使用各种编辑器构建设置。我们使用这些测试来验证复杂的编辑器集成、错误信息报告和项目构建大小(等等)。根据平台的不同,集成测试套件可在几小时内运行,通常至少每晚执行一次,甚至更频繁。

这些测试有什么影响?

在 Unity,我们的指导原则之一是 "解决棘手问题"。我喜欢从失败的角度来思考问题的难度。越是难以解决的问题,在找到解决方案之前,我需要完成的失败就越多。

在 Unity 中创建一个新的高性能、高便携性 AOT 编译器和虚拟机作为脚本后端是一个难题。不用说,一路走来,我们已经完成了成千上万次失败。有更多的问题需要解决,因此也会有更多的失败。但是,通过在一个全面而快速的测试套件中捕捉几乎所有这些失败中的有用信息,我们可以非常快速地进行迭代。

对于 IL2CPP 开发人员来说,我们的测试套件与其说是验证无错误代码的手段(尽管它确实能捕捉错误),或者是帮助将 IL2CPP 移植到多个平台的手段(它也能做到这一点),不如说是我们可以用来快速失败和解决棘手问题的工具,这样我们的用户就能专注于创造美好的事物。

结论

希望您喜欢 IL2CPP 内部结构系列文章。我们很乐意分享实施细节,并在可能的情况下提供调试和性能提示。如果您想了解更多有关 IL2CPP 设计和实施的其他主题,请告诉我们。