测试如何编写可维护的集成测试

Posted 软件工程之思

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了测试如何编写可维护的集成测试相关的知识,希望对你有一定的参考价值。

在软件开发行业,集成测试的编写某种程度上是最不受重视的工作。很多人仅仅会考虑单元测试项目,更多人可能根本没想过用自动化测试方法。编写一套可维护、可管理、可扩展的集成测试对大多数开发者来说是非常陌生的。

以前我也没体会到大型代码项目的集成测试的局限性,结果当我参与Voldemort项目,情况就不一样了。本文所讲的正是Voldemort集成测试的开发痛点,以及我们构建的新一代集成测试方案——Venice。


集成测试的痛点
Voldemort测试的两个主要痛点在于这些测试较为碎片化,出现错误会断断续续,而且测试跑起来也比较缓慢。这两项特性使这些测试不容易使用,可信度也不够高。

我们倒是有完全自动测试的框架,也深入研究了出错的测试的根源因素,不过我们还是经常跳过一些非常零碎的测试部分,虽然这并不是我们期望的。


测试每一个端口
Voldemort中,测试套件的碎片化与缓慢速度的核心原因在于端口的分配。我们的集成测试过程中,单台甚至整个Voldemort服务器集群会在本地启用,每个进程绑定到一个端口上。

有些Voldemort测试使用固定编码的端口号,短期来看倒是省事,长远来说就是麻烦了。被指定的端口可能在本地主机上已经足够繁忙了,有时两个指定到相同端口的测试会同时运行。让所有测试分别使用不同的固定端口实践上也是不可能的任务。


动态查找端口
有些Voldemort测试脑筋要灵活些,会动态化查找可用的端口。可是测试套件的其它部分并没有为这种方式优化架构,难以充分利用其优势。尽管在代码实现上多有不同,不过宏观来看多数测试会使用以下流程:

1. 调用 ServerUtils.findFreePorts() 来获取可用端口;
2. 在一个集群上调用 ServerUtils.getLocalCluster(int[] ports),监听第一步获取的端口;
3. 调用 ServerUtils.startVoldemortCluster() 启用第二步使用的集群中的不同服务器。

这组流程的问题在于第一步和第三步之间端口会被释放,于是某些情况下两个测试会同时监听到相同的可用端口,结果开始实际测试时就会因为撞车而双双崩溃。第三步中测试会多次重试启用崩溃的Voldemort服务器,然而如果一个端口被其它测试长期占用,这个步骤永不会完成。进一步来说,因为这些步骤说基于一个端口列表的,有可能出现大塞车的状况,两个各自独立的测试因为争抢端口而僵持在那里进退不得(直到各自到达最大重试限制,之后测试正式失败)。


测试的困境
理论上讲调整Voldemort测试代码以绕开上述问题是可行的,然而鉴于测试数量太多,预期成本也高到难以承受。于是现实的解决方案就成了让Gradle工具顺序运行所有测试以保证它们各自独立不被干扰。

然而非并行化意味着整套测试运行缓慢,意味着难以利用现代多核机器的潜能——本来多核系统最适合跑这类高并行化任务。

更重要的是顺序运行测试只是减少了端口冲突的几率,却没有根本上解决这个问题。例如其它测试有可能在共享的CI环境中同时运行。


开辟测试的新天地
动态查找端口的主意很好,但看起来Voldemort集成环境并没有很好地支持这一功能。因此我们是时候为未来设计新的测试方案了,也就是Venice。我们想看看能否在项目开始时就以全新的方案来解决问题。我们自问:“集成测试究竟需要什么?”


范式的变化
集成测试究竟需要什么?它们需要与特定进程交互。测试需要决定服务要使用哪些特定端口吗?或许用不着。那么为什么我们要把这些细节交给测试来处理?为什么不让测试框架提供一个已经绑定好的进程,而不论它在使用哪个端口?

这就是我们在新项目Venice中所做的事情。我们不再让测试开发者来考虑测试间冲突的麻烦,而是将这部分职责从开发者手中接管过来。测试可以向框架要求一个Kafka代理(broker)的包装(wrapper),一个Zookeeper服务器,或者其它什么需要的东西。然后测试可以从包装中查询运行包装流程的主机和端口是哪些。这套策略与Voldemort使用的有微妙的区别,然而这些差异改变了一切。


重试,有效的重试
这套方法的主要优势在于重试过程变得行之有效。Voldemort测试中一个进程绑定端口失败时会一次次故伎重来,等待端口被释放;Venice测试框架中,一旦进程绑定某个端口失败就会立刻跳出来寻找其它可用端口并绑定它。

于是乎,尽管动态查找端口的模式不太可靠,我们就能避开这一缺陷;不过前提是所涉及的代码的配置和自扩展算法都是端到端的。此外测试代码不能对进程需要的端口进行任何特定预配置。


性能/独立性的权衡理念
这套方案的另一个优势在于我们可以在性能和独立性中进行权衡。目前我们是基于最大独立性准则(例如每项需要进程的集成测试都会获得一个全新进程),但显然这很浪费资源,因为每个新启用的进程都有自己的启动时间。新方案中,我们就能实现不同的权衡配置。

例如,很多测试需要与Kafka代理交互。每个测试都能向框架请求一个Kafka代理并用它查询可用的主机与端口。不仅如此,我们会向框架询问测试该使用的Kafka主题(topic)名称。测试不需要关心要使用的是哪个主题,它只需要一个名称而已。这样如果我们的测试包足够巨大,我们可以用单个Kafka代理轮换应对所有测试,为所有需要主题名称的测试提供不同的名称,同时保证它们不会撞车。换句话说我们的测试请求资源、动态化处理获得的资源,但不去管无关紧要的细节——诸如主机、端口或主题名称。这样一来,我们就能灵活决定是要保证最大独立性(牺牲性能)还是让测试共享资源以提升速度。

进一步,我们甚至可以基于测试运行的环境来进行决策。例如一个CI的post-commit钩子能比开发者的电脑获取更多的计算资源。类似的,进行迭代工作的开发者可能需要快速评估测试是否通过,这里post-commit钩子可以异步处理,能够承受更多运行时间的代价。


展望
鉴于测试数量的增长,我们的测试包跑起来耗时越来越久,需要发展更快更高效的方法。开发一套设计出色的API来应对测试的资源请求使我们有足够的灵活性来继续进步。




以上是关于测试如何编写可维护的集成测试的主要内容,如果未能解决你的问题,请参考以下文章

如何在播放框架应用程序中进行完整的集成测试?

ZStack——自动化测试系统1:集成测试

Java集成测试中的void方法

集成测试 SQL 文件难以维护

Java如何优雅地实现单元测试与集成测试

Java应该如何优雅地实现单元测试与集成测试