当前位置: 首页 > 科技观察

使用契约优先开发减少契约测试

时间:2023-03-13 07:52:22 科技观察

作者|刘俊南合约维护问题如今,微服务以其灵活、易开发、易扩展等优势深入人心,不同服务之间的集成交互也越来越复杂。这些服务之间有多种交互方式,常见的有HTTP请求和消息队列。在他们交互的过程中,会发生服务版本演进,交互信息的格式或方式会发生变化。以前和以前版本的接口可能不兼容,甚至开发环境也会经常宕机更新。此外,不同服务的开发进度也不同。有快有慢,各队的优先级有高有低。在开发过程中,服务之间的交互方式匹配成为一个问题。在这里,不同团队之间关于如何在服务之间发送和接收消息的共同理解称为契约。如何采用合理的机制来维护服务之间的契约,让服务提供者和消费者在不引发意外的情况下保持各自的高效发展,越来越成为每个团队在日常开发中必须面对的问题。契约测试契约测试(contracttesting)就是在这样的背景下应运而生的,下面引用Pact官网的定义:契约测试是一种测试集成点的技术,通过隔离检查每个应用程序来确保其发送或接收的消息符合“合同”中记录的共同理解。契约测试是一种测试集成点的技术,它通过单独检查每个应用程序来确保它发送或接收的消息符合“契约”共同理解中记录的内容。也就是说,在测试我们自己的服务时,通过使用测试替身,可以模仿我们所依赖的外部系统,返回相对真实的消息响应,让我们的团队在尽可能保证与外部系统兼容的前提下避免外部系统宕机或开发新版本等影响,提高开发效率。结合消费者驱动开发的优势,可以避免在服务提供者端浪费精力去实现不必要的功能。因此,很多团队采用消费者驱动契约测试(consumer-drivencontracttest)的做法。借助契约测试,很多团队确实提高了开发效率,掌握了自己的节奏,但也有一些团队发现效果并不明显,因为契约测试带来的收益不是免费的。合同测试有很多开发成本。每次有新的需求,新的接口,新的字段,或者旧字段的可空性发生变化,以及枚举值的增减,都需要增减一些测试用例,然后在测试过程中生成新的合约。大多数这些合同都以OpenAPI文档的形式存在,作为两个团队未来讨论的基准。合约测试驱动的合作流程消费者与提供者通信达成一个基本合约:添加一个接口,调用时传递一个RequestDto,接口返回一个ResponseDto,RequestDto和ResponseDto哪些字段必须非空。consumer回过头来写自己的contracttest,生成一个contract(一般是OpenApidoc的形式),然后用contracttest去驱动和开发自己的逻辑。服务方获取生成的契约,进行测试驱动开发,验证契约是否满足。合约测试有时修改成本很高。我在使用契约测试的时候经常有这样的感觉:比如一些简单的契约,比如非空字段、格式验证,需要每个case都专门测试用例,但是只需要一个注解就可以实现。有点吃亏。更重要的是,随着测试的编写,生成的合约可能比当时讨论的更简单,比如一些400、401等,有时无法针对每个API编写足够详细和详细的测试;也可能生成的合约比讨论的更详细,比如消费者在写合约测试的过程中考虑了更多的边缘场景。所以,每当合约因为上述情况修改后,我们会重新沟通,等待消费者重写合约测试,然后生成合约,然后服务提供者再开发。这个从通信到落地的闭环比较长。每次修改,服务提供者需要等待消费者编写测试生成契约,然后消费者等待服务端根据契约完成开发,然后发布新版本。这些等待会降低开发效率。想法解决的反馈周期长是我们经常需要面对的问题。例如,我们需要进行快速迭代和定期展示,以便及时得到客户的反馈;提前,避免盲目开始后的返工。这时候我们再看开发过程,我们会想,如果第一步的沟通可以直接产生一个固化的合约,足够直观和详细,让双方早点沟通,同时时间,我们可以通过自动化的方式约束消费者和服务方,省去重复繁琐的合约测试,让双方沟通后直接开始开发。Contract-firstdevelopment这里所说的contract-first是指在写完所有代码之前,先把我们通信的contract写出来,或者通过一些图形工具生成,比如手写或者生成一个yaml格式的OpenAPI文档,这样通信的输出就是足够直观。在代码库中维护这个文档,然后以此为基础,通过自动化流水线为各方生成通信组件(sdk),可以让双方同时开始开发,也可以对双方进行约束。下面以Java+Spring为例,通过使用OpenAPI生成器工具生成代码来实现上述效果。对于消费者,管道可以根据建立的契约生成一个封装的客户端类,它提供了一个简单的方法,包含了RestTemplate的参数和逻辑,以及对应的RequestDto/ResponseDto。消费者端的开发者只需要关注自己的业务需求,将合适的参数传递给该方法即可,无论这些参数是用于path还是body,该方法都会以合适的方式与服务端通信。同时,我们可以自定义生成方法来检查非空字段,达到约束效果。对于服务端,管道可以生成相应的Controller组件,匹配http路径和方法等,服务端只需要重写相应的方法即可完成自己的业务需求,不需要关心传入的参数是否正确属于路径、标题或正文。由于自定义生成的服务端sdk严格按照约定生成合适的注解,如@NotNull、@Size、@Pattern等,Spring可以自动验证注解,服务端可以自动拒绝请求不符合合同。同时,由于生成的ResponseDto也有响应验证注解,我们也可以对服务端返回的ResponseDto进行约束。在契约优先模式下,团队的沟通是闭环的。这样,团队的合作流程是:消费者与提供者沟通,达成合约,在合约代码库即OpenAPIdoc中一起提交合约代码。然后触发管道为各方生成代码sdk。双方各自引入SDK进行开发。通过观察以上流程可以发现,与合同测试相比,这个流程直观地将沟通结果提前固化,让双方可以根据细节提前沟通,从而缩短反馈周期,降低返工概率.此外,一旦提交合约,自动生成的服务端和客户端SDK也同时可用,消除了开发过程中消费者与服务端的依赖。两端可并行开发,减少等待时间,提高开发效率。如果在开发过程中,任何一方有细节问题需要调整合约,您可以尽快找到对方协商。此时,由于双方已经进入开发阶段,也了解了相应的一些细节,所以讨论的内容更加具体、效率更高,讨论产生的合同变更也会更早生效。Contract-first适用场景Contract-first开发不是灵丹妙药。解决特定场景下的问题更“划算”。例如,合同应该简单明了。一些非空检查,格式要求,简单的字段匹配,使用契约优先,生成代码都是低投入高回报,而且生成的代码限制性很大。但是,如果合约包含丰富的业务逻辑,则不容易在单个OpenAPI文档中对其进行描述。手写合约测试更加清晰易维护。再比如,我们使用的编程语言或框架需要得到OpenAPI生成器的良好支持。如果你不能按照约定生成有用的代码,或者你在生成代码的过程中需要做太多的定制,那么这种方法可能不适用或者不划算。开发过程中需要进行完善的集成测试或组件测试。在上面生成的代码中,虽然sdk可以对服务之间的通信进行法律约束,但是很多单元测试无法提前发现问题。比如在服务端,我们生成的@NotNull等注解需要启动Spring,测试用例对业务逻辑有足够的覆盖,才能及早发现问题,避免线上报错。合同优先开发的成本天下没有免费的午餐。在带来上述优势的同时,契约优先开发也会带来一些成本。主要成本是OpenAPI生成器的学习成本。目前,虽然OpenAPI生成器已经可以支持大部分语言和框架,但是需要对生成的代码进行一定的定制,以使其易于使用。这些定制需要一些时间投入。结论在服务间协同开发的过程中,为了维护契约的有效性,契约测试的应用可以在一定程度上解耦不同团队之间的开发。在某些场景下,使用契约优先合作可能会更高效。例如,如果契约简单明了,用于开发的技术适合生成的代码,并且在开发过程中有足够的集成测试或组件测试,contract-first可以缩短团队之间的反馈闭环减少等待时间,提高开发效率。