前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >契约测试:微服务完整应用系统验证之道

契约测试:微服务完整应用系统验证之道

作者头像
用户1682855
发布2019-08-01 11:06:42
1.4K0
发布2019-08-01 11:06:42
举报
文章被收录于专栏:前沿技墅前沿技墅

单元测试、组件测试和集成测试的一个共同特点是,会将应用的某一部分隔离开来去测试,而不是测试整个完整的应用。对于单元测试,被测单元只有一个或者很少几个类 ;对于集成测试,你在应用的边界测试应用是否可以连接到一个真实的服务。而要在整个应用的基础上来编写测试,则离不开契约测试。本文重点阐释使用契约测试来对整个系统进行验证的重要性,以及如何编写契约测试。

理解契约

微服务的架构包含了大量彼此相互通信的微服务。在 Gamer 应用中可以看到聚合服务、视频服务、评论服务等其他服务之间的相互通信。这些交互形成了服务之间的契约,契约包含了预期的输入 / 输出数据以及前置 / 后置条件。

服务之间数据消费的规范构成了契约,提供(或生产)数据的一方需要按照约定来提供数据。如果生产数据的服务发生了变化,生产者必须保证和消费它所提供数据的消费者之间的契约依然符合预期。契约测试提供了一种机制,可以显式检验一个组件是否满足契约。

  • 契约和单体应用

在单体应用中,所有的服务被部署到同一个项目中。每一个服务在一个单独的模块或者子项目中开发,运行在同一个运行时(runtime)中。

对于这类应用,你不需要担心服务之间的契约(或者兼容性)被打破,因为编译器会帮助你隐式地校验这些问题。一旦一个服务破坏了契约,编译器会抛出编译错误来拒绝构建。下面让我们来看一个例子。这是 serviceA.jar :

public class ServiceA {

void createMessage(String parameterA, String parameterB) {}

}

这是 serviceB.jar :

两个服务 A 和 B,被生成两个不同的 JAR 文件。服务 A 通过传入 create-Message 方法的两个 String 类型参数来调用服务 B。这个方法就是这两个服务之间的契约。

但是,如果服务 A 像下面这样更改了契约会发生什么呢?

服务的签名变为了一个 String 类型的参数和一个 Integer 类型的参数。这破坏了和服务 B(服务 A 的消费者)的兼容性。对于单体应用来说这并不是一个问题,会出现编译错误信息 :方法 createMessage 需要(String,In-teger)参数,但是在这里只是发现了(String,String)。因此,我们可以很容易地发现契约失效的问题。

从测试的角度来看可以通过使用 new 关键字或使用上下文依赖注入(CDI),或者通过 Arquillian 和 Spring 测试框架来使用 Spring 控制反转(IoC)等方法,以便在测试逻辑中使用一个模块。但是对于微服务架构来说,事情变得更复杂且更难探测。如果两个服务之间的契约被破坏了,则可能在很长一段时间内都不会被发现。

  • 契约和微服务应用

每一个微服务都拥有自己的生命周期、自己的运行时环境,并独立于其他微服务。在这种场景下,对于一个服务契约的更改无法被编译器检测到。下图 展示了多个运行时的多个服务。

样例应用的整体视图

服务之间的不兼容很可能发生且很难被检测。由于你不会直接得到异常的反馈,因此兼容性很容易被破坏。由于缺少测试,因此问题很难被发现,很可能在生产环境才会发现该问题。

通常情况下,每个服务都是由不同的团队部署维护的,如果团队之间的沟通不流畅的话,兼容性的问题会更复杂、更难以被检测。从我们的经验来看,最普通的问题是,生产者一方进行了更改,以致消费者无法和生产者正常通信。

最常见的问题如下 :

  • 服务更改了接入点 URL。
  • 服务新增了一个必填的参数。
  • 服务修改 / 删除了一个已有的参数。
  • 服务修改了对输入参数的校验逻辑。
  • 服务修改了返回类型或者状态码。

想象下面一个例子,这里有两个服务 :生产者和消费者 A。生产者服务暴露了一个 JSON 格式的 blog-post 资源,消费者 A 对其进行消费。

一个可能的文档如下所示 :

{

"id" : 1,

"body" : "Hello World",

"created" : "05/12/2012",

"author" : "Ada"

}

这条消息包含 4 个属性:id 、 body 、created 和 author 。消费者 A 只需要body 和 author 字段,并忽略其他字段,如下图所示。

生产者和消费者 A 之间的数据交换

一段时间后,一个新的消费者需要消费生产者的资源 API。新的消费者,即消费者 B,需要 author 和 body 字段,同时需要一个新的( author id )字段作为博客作者的标识符。

此时,生产者的维护者有两种不同的方法 :

  • 在根级别新增一个字段。
  • 创建一个包含 author 字段的复合对象。

对于第一种方法,需要在文档和 author 同级别的地方增加一个 authorId 字段。一个可能的文档如下所示 :

{

"id" : 1,

"body" : "Hello World",

"created" : "05/12/2012",

"author" : "Ada",

"authorId" : "123"

}

通过这个变更,消费者 B 的需求得到了满足。如果消费者 A 遵循了 Postel 原则(Postel’s Law),它也可以继续成功消费生产者服务的消息。

健壮性原则

健壮性原则又被称为 Postel 原则,它来源于 Jon Postel。在他编写 TCP 协议早期规范时提及 :对你发送的内容要严苛,对你接收的内容要宽容。对于 HTML 来说这是一个糟糕的原则,它产生了荒谬的浏览器“战争”,现在我们通过更为严格的 HTML5 规范才解决了该问题。但是对于数据解析来说,这个原则依然是正确的。对于我们这个例子来说,生产者和消费者都应该忽略那些对它们来说不重要的字段。

下图展示了两个消费者是如何消费生产者的消息的。假如维护者选择使用第二种方式,并创建了下面一个拥有 authorInfo 字段的复合对象 :

{

"id" : 1,

"body" : "Hello World",

"created" : "05/12/2012",

"author" : {

"name" : "Ada",

"id" : "123"

}

}

生产者和消费者 A、消费者 B 之间的数据交换

在这个变更下,消费者 B 的需求得到了满足,但是这个变更破坏了和消费者 A的兼容性。在下图中可以看到,尽管消费者 B 可以正确地处理生产者的消息,但是消费者 A 却无法做到这一点,它预期的是一个 string 类型的 author 字段。

更新数据交换格式

如果你使用的是单体应用,这个问题在编译期就会被发现,但是在微服务的架构下却无法立刻知道错误的发生。从生产者的角度来看,即使所有的测试都通过了,你依然无法知道契约是否被破坏了。

当生产者服务被部署到一个完整的、所有服务运行正常的环境中时,问题会发生在消费者服务一端。这时,因为契约被打破,所以消费者就会工作异常。这里需要对所有之前适配过生产者 API 但是现在失败的消费者服务进行修复。

问题发现得越晚,就越难修复 — 在应用部署的不同阶段,问题的严重程度也会不同。假设问题在生产环境才被发现。这时你需要回滚生产者服务到一个旧的版本 ;同时,所有被更新过的消费者也要进行回滚,以确保整个环境可以正常工作。你需要花费大量的时间来检查部署失败的原因并进行修复。下图展示了在项目的不同阶段发现一个 bug 并进行修复所需的成本。

在开发的特定阶段修复 bug 的成本

使用微服务架构意味着我们需要改变服务测试的方式,这样才能在生产者服务上线前发现这些问题。理想情况下,bug 应该在测试的 CI/CD 阶段就被发现。

废弃方法

你可以同时使用这两种方式来解决这个问题,将 author 字段废弃而不是移除。新的文档格式如下所示 :

我们新建了一个 authorInfo 字段 ;同时 author 字段得以保留,但是被标记为废弃。这种方法不用更改任何测试,但是当你打算彻底移除废弃字段时却会碰到同样的问题。不过,至少你有了一段过渡时期,可以通知消费者的维护者进行更改来适配新的格式。

  • 使用集成测试进行验证

如果了解如何使用集成测试来测试一个系统是否能和另一个系统正常通信,从契约的角度来看,你就是在测试消费者的边界或者网关类,是否可以通过正确地和一个生产者进行通信,来发送或者获取数据。

你可能会认为集成测试已经覆盖了契约被破坏的用例。但是这种方法有一些缺陷,这会导致运行这类测试变得困难。

首先,消费者必须知道如何启动生产者。其次消费者可能会依赖多个生产者,每个生产者都可能有自己的一些依赖,例如数据库或者其他服务。因此,启动一个生产者可能会导致启动多个服务,如果不加注意的话会将集成测试演变成端到端测试。再次,也是最重要的一个问题是,你需要维护生产者和所有消费者的关系。当对生产者进行变更的时候,集成测试需要运行所有相关消费者测试来确保它们和生产者之间的通信。这个机制难以维护,因为每当新增一个消费者时,你都要通知生产者团队,并提供一组新的测试。

尽管集成测试是一种检验生产者和消费者之间通信的方案,但是很多时候它都不是最优的策略。

  • 什么是契约测试

如之前所提到的,契约是客户端(或消费者)服务和生产者服务之间的一组约定。契约的存在定义了每个消费者和生产者之间的交互规则,这可以解决 上文中的所有问题。

下图展示了消费者和生产者之间的一个契约——它是一个文件,描述了生产者和消费者需要遵循的规则。现在生产者和消费者之间通过契约来连接,而不是直接相连。从生产者的角度来看,它只需要满足契约中的规定。因此,生产者不需要运行所有消费者的集成测试 ;你只需要测试消费者可以消费符合契约的请求并生成符合契约的响应。

生产和消费者之间的交互

在这个例子中,生产者和所有消费者之间有一个或多个数据相关的契约。对于生产者的每个变更,必须验证所有契约是否都没有被打破,而这无须进行集成测试。

我们还需要在消费者端进行契约的校验,以验证客户端(网关)类是否符合契约。需要注意的是,这时候你依然不需要知道如何启动一个生产者或者启动任何的外部依赖,因为契约的检验无须启动生产者;你只需要检验消费者是否也满足契约的要求。

对契约进行检验的测试也被称为契约测试。接下来的一个重要问题是,谁应该负责创建并维护契约文件?下面我们来解决这个问题。

  • 谁负责契约

你已经了解了,验证消费者和生产者之间可以正确且持续通信的最好方式是定义它们之间的契约。但是我们并没有解决谁应该负责契约,是生产者团队,还是消费者团队?

  • 什么是生产者契约

如果由开发生产者的团队来负责契约,这就意味着他们不仅需要了解自己服务(生产者)的逻辑,还需要了解他们支持的所有消费者的业务。因为这种契约属于生产者,所以这种契约也被称为生产者契约,消费者只能查看这些契约,如下图所示。一个这种契约可以发挥优势的例子是内部的安全身份验证 / 鉴权服务,所有的消费者服务都必须遵循生产者的契约。

生产者契约

生产者契约定义了生产者会提供什么样的数据给消费者。每个消费者都必须适配生产者提供的数据。这就意味着生产者和消费者存在耦合,如果生产者团队提供的契约无法满足某个消费者的需求,那么消费者服务团队必须和生产者服务的团队进行沟通来解决这个问题。

  • 什么是消费者契约

从另一方面来说,为了解决这种一刀切的契约,又不需要强制生产者团队定义一个完整的契约,你可以将契约的生成和维护放在消费者服务的开发团队,消费者开发团队定义他们需要的契约并提供给生产者团队去实现。这样契约因为由消费者团队负责,所以也被称为消费者契约。

消费者契约从消费者的角度定义了规范。因此,契约只适用于单个消费者的特殊使用场景。消费者契约可以作为已存在的生产者契约的一个补充,或者帮助生产者创建一个新的生产者契约。下图展示了每一个消费者和生产者之间都存在一个契约,而不是一个适用于所有消费者的契约。

消费者契约

这种契约可以发挥优势的一个例子是组织内部的检出服务,这个服务的演进受控于生产者,但是从生产者拉取的数据会在多个上下文中使用。这些上下文都会独立地演进,但是它们有一个内在的控制和演化轨迹。

  • 什么是消费者驱动的契约

消费者驱动的契约是生产者和所有消费者契约的一个聚合契约,如下图所示。很明显,生产者可以通过创建一个满足所有消费者需求的生产者契约来对消费者契约进行扩展。

消费者驱动的契约

消费者驱动的契约建立在生产者服务从消费者的视角来进行开发。消费者需要和生产者沟通消费者的需求,以满足消费者的使用。生产者有责任满足这些需求,以符合所有消费者的预期。

理想情况下,契约测试应该由消费者团队开发并发送给生产者团队,生产者团队再据此来开发生产者服务。将消费者契约归属于消费者团队可以确保生产者的消费者驱动契约正是消费者所需要的,而不是生产者认为消费者应该需要的。此外,当生产者服务的维护者更改生产者服务代码时,他们知道变更对消费者的影响,可以确保当新版本的服务上线时不会对消费者的通信产生影响。

消费者端也要担负一些责任。生产者负责遵守消费者驱动的契约,消费者需要确保自己不多不少地遵循了契约。消费者应该只从生产者那里消费自己所需要的数据。通过这种方式,消费者可以保护自己不会受到生产者添加字段引起的消费者契约变化。

这种契约可以发挥优势的一个例子是对外部(对组织外开放)或者内部多个组织之间使用的用户服务。从用户服务获取的数据被用于多个上下文中。这些上下文都会独立发展,并遵循外在的控制和演化轨迹。

到现在为止,讨论了为什么对于运行在多个运行时环境的服务,集成测试并不能满足所有场景,以及为什么消费者驱动的契约可以解决生产者服务更新导致的通信问题。下面让我们看一下如何实践这些原则,以及我们可以使用哪些工具来针对微服务架构使用消费者驱动的契约模式。

工具

我们已经解释了契约测试对于微服务架构下避免生产环境的故障十分重要的原因。接下来让我们看一下有哪些工具可以帮助我们编写契约测试。这里有 3 个最流行的工具 :

  • Spring Cloud Contract——Spring 生态系统下一个使用 Groovy 的测试框架。尽管它最初是为了和 Spring 产品集成,但它也可以单独和任何使用 JVM 语言开发的应用集成。
  • Pact——一系列支持消费者契约测试的测试框架。它官方支持 Ruby、基于JVM 的语言、.NET、JavaScript、Go、Python、Objective-C、PHP 和 Swift。
  • Pacto——一个用来开发消费者驱动和文档驱动契约的框架。它是用 Ruby 开发的,但是可以通过 Pacto 服务器在其他语言如 Python 和 Java 中使用。

在我们看来,Pact(https://docs.pact.io)是契约测试领域使用得最为广泛也是最成熟的项目。它的一个主要优点是支持几乎所有编写微服务的主流语言,相同的概念可以被前端和后端复用而无须考虑编程语言。鉴于这些原因,我们认为 Pact 是编写消费者驱动的契约最为明智的解决方案。它和 Java 开发的微服务可以很好地进行集成。

本文选自《Java微服务测试:基于Arquillian、Hoverfly、AssertJ、JUnit、Selenium与Mockito》,书中Red Hat的Java 大师、Arquillian作者为我们开启测试方案、描述与样例的恢弘画卷,用来自真实世界的案例与技术,让读者饱览微服务测试技术的全景与细部。阅读原文直指这一最佳测试覆盖率与生产力的灯塔,习惯夜航的Java开发者,从此不再迷失方向。

内容简介:本书从实战出发,介绍微服务架构所带来的测试方面的挑战,以及如何利用新的技术来应对这些挑战。通过本书,读者可以学会如何编写微服务架构下的单元测试、组件测试、集成测试及契约测试。其间还会用到Arquillian、ShrinkWrap、Pact、Selenium、Docker、Hoverfly 等多个帮助测试的工具和框架。书中涵盖大量的代码和样例,可以帮助读者快速上手,并在自己的实际工作中应用这些技术。本书适合有一定Java 基础的开发和测试人员,对使用其他编程语言的开发者也会有一定的帮助。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-07-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前沿技墅 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
多因子身份认证
多因子身份认证(Multi-factor Authentication Service,MFAS)的目的是建立一个多层次的防御体系,通过结合两种或三种认证因子(基于记忆的/基于持有物的/基于生物特征的认证因子)验证访问者的身份,使系统或资源更加安全。攻击者即使破解单一因子(如口令、人脸),应用的安全依然可以得到保障。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档