在上一节里,我们学习了微服务的来源和主要特点。对于软件测试人员而言,微服务架构对软件测试带来了哪些新的挑战呢?我们应该用什么样的策略和方法来迎接这些挑战?
总体的测试策略
软件测试的目的是确保软件产品的质量符合预期。衡量测试质量的指标有很多,最常见的是测试覆盖率和测试成本(包括测试所用时间、测试维护成本),而衡量测试效果的主要手段则是最终产品在实际使用中暴露出来的问题数量(Bug Number)。
具体到采用微服务架构的产品而言,Martin Fowler 在关于软件测试的论述中提出了其目的:
开发团队采用的任何测试策略,都应当力求为服务内部每个模块的完整性,以及每个模块之间、各个服务之间的交互,提供全面的测试覆盖率,同时还要保持测试的轻便快捷。
因此,我们需要采取下面几点测试策略:
- 我们一方面要保证从各个维度上,无一遗漏地对微服务进行全面的测试,特别是对于分布式的系统,系统的所有层次都必须被覆盖到;另一方面又要确保测试执行的快捷,这样才能保证持续集成/持续交付(CI/CD)的实现。
- 要确保测试策略的正确实施,工具和技术固然重要,然而,首先需要测试人员在团队中树立起提倡质量第一的“测试文化”:
- 无法通过测试的代码不应该被合并到代码仓库里;
- 无法通过测试的代码不应该被发布出去。
- 不能为了测试而测试,测试的真正目的是为了交付高质量的软件给用户,而不是把资源浪费在没有实际意义的测试用例上。所有的测试层次、流程和用例,都应该有的放矢。
传统测试方法面临的挑战
以一个常见的开发团队为例,在采用了微服务架构之后,很可能同时会开发多个模块(即微服务),每个微服务有不同的客户要求、开发周期、开发进度和交付期限,但是整个团队又必须保证能够在固定的时间节点(譬如每月一次、每两周一次,甚至每天一次或者多次),持续地、稳定地为用户提供可以部署、使用的产品。这意味着,过去那种先等产品经理、业务部门提供需求,开发人员再进行开发,最后交给测试人员执行集成测试、端到端测试的方法,已经无法提供足够的测试粒度和足够快的响应速度。
归结起来,与基于单体式架构的传统测试方法相比,微服务架构对测试提出了以下挑战:
- 服务/模块/层次(layer)之间存在复杂的依赖性。
- 在单体式架构中,通常使用集成测试来验证依赖是否正常。而在微服务架构中,服务数量往往很多,每个服务都是独立的业务单元,服务之间主要通过接口进行交互,如何保证这些依赖的正常,是测试人员面临的主要挑战。这意味着,如果想单独测试某一个服务,或者服务中的某个模块,就必须剥离它们对于其他环节的依赖关系。这需要通过 Mock、Stub 等方法来实现。
- 不同的服务可能会在不同的环境/设置下运行。
- 特别是一些后端服务,与前端服务的运行环境可能截然不同。这时在考虑对每种服务设立自动化管线时,就必须有针对性的设置相应的环境配置。而且,在微服务架构中,每个服务都独立部署,交付周期短且频率高,人工部署已经无法适应业务的快速变化。因此如何有效地构建自动化部署体系,保证配置的稳定性、可重复性,是微服务测试面临的另一个挑战,必须与 DevOps 人员一同解决。
- 涉及多个服务的 UI 端到端测试(End-to-End 测试,简称 E2E 测试)非常容易出错。
- 因为每种服务的开发进度不同,集成不同服务的端到端测试往往会因为某一个服务的微小改动而出错。这种出错是测试人员希望避免的干扰信息。这意味着,对端到端测试的设计,必须采取一定的防干扰、防误报策略。
- 测试结果可能取决于网络的稳定性。
- 微服务架构是基于分布式的系统,而构建分布式系统必然会带来额外的开销。
- 性能: 分布式系统是跨进程、跨网络的调用,受网络延迟和带宽的影响。
- 可靠性: 由于高度依赖于网络状况,任何一次的远程调用都有可能失败,随着服务的增多还会出现更多的潜在故障点。因此,如何提高系统的可靠性、降低因网络引起的故障率,是系统构建的一大挑战。
- 异步: 异步通信大大增加了功能实现的复杂度,并且伴随着定位难、调试难等问题。
- 数据一致性: 要保证分布式系统的数据强一致性,成本是非常高的,需要在 C(一致性)A(可用性)P(分区容错性)三者之间做出权衡。
- 特别是涉及到数据存储和外部通信的部分,如果在测试中不摆脱这些因素的影响,就可能会得到一些随机性的误报,干扰测试结果。
- 微服务架构是基于分布式的系统,而构建分布式系统必然会带来额外的开销。
- 故障分析的复杂度会随着服务的增加而提高。
- 微服务架构中,因为每个服务都需要独立地配置、部署、监控和收集日志,因此在发现问题之后,进行诊断分析时,搜集缺陷信息的成本呈指数级增长。
- 与交付周期不同的开发团队之间的交流成本。
- 这一点虽然跟技术无关,但是实际上会对测试人员的工作造成很大的困扰。因为开发模式分解为负责不同服务的多个小组,测试人员往往每天要花费大量的时间,了解不同团队的开发进度。如果还需要手动进行回归测试(Regression Test),最终将会不堪重负。所以自动化测试是必须采取的手段和方向。
如何应对这些挑战,我总结了下面这三个原则:
1.自动化:测试任务的增加,要求测试人员必须把主要的精力用于将测试自动化,摆脱手动测试带来的沉重负担。当然,自动化测试必须足够稳定、稳健,不能动辄误报,否则反而会导致很高的维护成本。
2.层次化:这意味着采用分层次的测试方法,粒度由细到粗,范围由小到大。下图说明了几个主要层次之间的关系:
这就是 Mike Cohn 提出的测试金字塔(Test Pyramid),其中最重要的两个原则是:
- 应该用不同的粒度来测试应用程序;
- 层次越高,测试越少。
最底层的是单元测试(Unit Test),粒度最细,速度最快,维护成本也最低。往上是针对每种服务内部的各种模块、业务流程的测试。最上面是基于前端 UI 的测试,这部分的粒度最粗,范围最大(因为会覆盖大多数服务),但是维护成本最高,因为稍微有些细微的变化就可能需要调整脚本。而且,由于基于前端,需要设置很多响应时间和等待时间,所以速度最慢。
Mike Cohn 是 Scrum 软件开发方法的提出者之一,也是 Scrum 联盟的创始成员。他目前是 Mountain Goat Software 公司的所有者,致力于提供关于 Scrum 和 Agile 软件开发技术的培训。
3.可视化:为了降低交流成本,最好的办法就是让所有的测试结果可视化。这意味着将构建(Build)、测试(Test)、部署(Deploy)所有这些相关任务构建在一个流水线之中,让所有团队成员都可以随时监控项目进度,找到阻碍项目的瓶颈。
以下面这个典型团队为例,整个从开发、测试、构建到部署的一系列过程,都可以借助 Jenkins 或者 TeamCity 这样的任务调度工具,完全可视化,再借助 SonarQube 这样的代码质量监控工具监控测试结果。Google Analytics 或者 Microsoft 的 Azure ApplicationInsight 等云端监控工具,则可以提供实时生产环境的客户使用信息或者测试数据,让整个团队可以随时把握产品的整个流水线的运行状态。
本达人课的后面几节内容,将会以层次化的方式,逐一介绍在微服务架构中所采用的主要测试方法。如下图所示,它们主要包括:
- 单元测试(Unit Test)
- 用于验证微服务内部的类方法或函数的行为。它们会根据测试框架,执行代码文件里的类方法或函数,提供不同的输入,并验证与每一个输入相对应的输出。
- 集成测试(Integration Test)
- 用于验证微服务与外部模块的通信或者交互行为。测试框架会启动服务的一个实例,并调用服务的外部接口来执行业务逻辑。
- 组件测试 (Component Test)
- 即验证微服务能否起到预期的作用。这需要把微服务周边依赖的所有其他服务或者资源全部模拟化,从该服务外部“用户”的角度来检查服务能否提供预期的输出。
- 端到端测试(End-to-end Test)
- 验证整个系统的功能能否符合用户的预期,一般是从 UI 层面进行测试,确保用户体验完全达到客户要求。
-
探索测试( Exploratory Test,即手动测试)
- 这一步通常由业务专家型用户执行,具体查看某个新添加的特性是否开发、部署成功。
本节总结
- 微服务架构对软件测试提出了很多全新的挑战。
- 应对这些挑战的方法包括:
- 自动化
- 层次化
- 可视化