4.5. 可测试性设计

单元测试的重要性不言而喻,这是所有测试的第一道关,是所有测试的基础。在微服务下会有很多个服务,对各服务组件自身的测试也会很重要,组件测试会覆盖当前服务的所有对外接口实现对各个服务的功能边界验证。单元测试和组件测试可以很好地发现各单点或各服务的问题,但它无法检查系统的流程行为,而这就需要集成测试来验证组件之间的通信路径和交互,集成测试关注的一次请求或一个业务流程是否正常,集成测试多针对接口,某些情况下我们可能会特别关注用户交互,而UI测试(或End-To-End测试)可以比较好发现这类问题。

测试金字塔 让我们可以直观了解到从单元测试到组件、集成测试再到UI测试,测试的价值逐级升高,但同时测试的难度、响应的速度也在逐级递增,最Top的UI测试最能直观的反馈问题,但此测试构建及维护难度很大,不是一般团队所能承受,反观单元测试却是最简单的可快速验证的,所以单元测试是基础,项目中必须予以重视。

当我们有了上述针对不同目标主体的测试时可以有效地提升测试的覆盖率,确保各服务的输出符合业务需求。这看上去很完美了,但是我们的系统功能、接口行为真的符合使用方的预期吗?其实上述测试都是基于产品需求所做的合理用例下的测试,即这些用例并不直接来源自使用方,而后者却更能真实反应产品服务的价值及期望。所以就有了消费者驱动契约测试CDCT

微服务下我们会面临以下两个重要问题:

  • 分团队开发,服务的提供有快慢,怎么让消费者不用等着依赖服务开发完成?
  • 怎么保证接口的变更可以适配所有消费者?

第一个问题比较好办,为减少开发期间的依赖,我们只要约定好依赖服务的接口契约,消费者在规定的契约下进行开发就可以了,这也大家普遍的做法,但谁来定个契约?第二个问题可以参考上文接口版本变更的处理,尽量使用兼容方案,但如果需要修改原字段定义时上文只说要通知所有消费者,有没有更优雅的方法让我们只需要通知受影响的消费者?

解决上面两个问题的核心是明确接口契约,记录哪个消费者在用,用了哪些参数,这时我们谈消费者驱动契约的方案水到渠成了。消费者驱动契约的核心由消费者定义接口契约,针对同一接口,不同消费者需要的字段会有差异,由消费者自己定义需要什么,这样在后期接口变更时就可以评估影响范围了。消费者驱动契约测试就是在这样的场景下提出的,目前比较流行的工具有Pact、Pacto及Spring Cloud Contract,以Spring Cloud Contract为例,它的核心是编写由消费者提供的接口契约,支持groovy及YAML,下面是其官网的契约示例:

org.springframework.cloud.contract.spec.Contract.make {
  request { // 定义请求部分
    method 'PUT' // PUT请求
    url '/fraudcheck' // 对应的URL
    body(""" // 请求的Body体
    {
      "clientId":"1234567890",
      "loanAmount":99999
    }
    """)
    headers { // 请求的Header
      header('Content-Type', 'application/vnd.fraud.v1+json')
    }
  }
  response { // 定义对应的响应内容
    status 200 // 返回成功状态码
    body(""" // 返回体
    {
      "fraudCheckStatus": "FRAUD",
      "rejectionReason": "Amount too high"
    }
    """)
    headers { // 返回Header
      header('Content-Type': 'application/vnd.fraud.v1+json')
    }
  }
}

服务可以根据此契约生成Mock对象提供给消费者使用。CDCT是比较新生的概念,规模使用中成功的案例相对比较匮乏,读者需要斟酌是否需要尝试。

谈到测试就不得不聊一下TDD(测试驱动开发),TDD把测试的重要程度进一步提升,提出测试先行的观点,它要求将一个个需求转换成对应的测试用例,开发流程是先写测试用例,只定义输入及期望的输出,此时执行必定失败,然后编码仅针对此用例的业务代码确保测试用例通过,最后做一些代码优化结束当前需求的开发,TDD要求不断地重复这个流程以完成各个需求的迭代。TDD可有效地提升代码的质量,但了批评者认为其导致开发者只专注于用例实现而忽视实际需求及架构设计、降低了交付速度等,笔者认为TDD的方法论在微服务下可以适度借鉴,让核心服务或团队走TDD,其它服务或团队因地制宜。

测试的重要性再怎么强调都不过分,软件工程发展至今也演化出了不同种类的测试,除上我们常规的单元测试、组件测试、集成测试、UI测试外,也产生了诸如CDCT、TDD、BDD(行为驱动开发)、ATDD(验收测试驱动开发)等方法论,在实践中需要我们结合团队及项目产品的情况做出合理的选择,就笔者经验而言单元测试是必选项目,是对开发人员的强制要求,核心的功能会考虑走TDD,核心的服务要求有完整的组件测试,大部分的项目也会要求有自动化的集成测试,但几乎没有成功实施过UI测试,CDCT是未来值得尝试的方向。

下一节:传统的服务运维对自动化的要求不需要太高,但使用微服务后服务的数量会急剧上升,一个成熟的微服务可能有几十到几百个组件,每个组件做HA,最终可能有成百上千个实例,同时还要考虑开发、测试、UAT/预发、生产、灰度等环境,人工部署将是灾难性的。