写点什么

利用 Diferencia 和 Java 微服务进行分接比较测试

作者:Alex Soto

  • 2019-03-11
  • 本文字数:6762 字

    阅读完需:约 22 分钟

利用Diferencia和Java微服务进行分接比较测试

本文要点

  • 在微服务体系结构中,许多服务可能同时在(相对)独立地演化,而且通常非常迅速。要获得这种架构风格的全部价值,服务必须能够独立发布。

  • 通常很难验证新服务(或服务的新版本)没有对当前的应用程序造成任何破坏,即 API、载荷或响应性能的变化导致回归。

  • “分接比较(Tap compare)”是一种测试技术,它使你可以通过把新服务的结果与旧服务进行比较来测试新服务的行为和性能。本文提供了一个使用新开源工具 Diferencia 的示例,通过在新旧服务之间镜像生产流量来比较结果的差异。

  • Diferencia 是一个用 Go 编写的开源工具(遵循 Apache v2 许可),它与 JUnit 4、JUnit 5 或 AssertJ 等 Java 测试框架紧密集成,让你可以使用分接比较测试技术来验证服务的两个实现在语法上是否兼容。


DevOps 在过去几年中越来越受欢迎,特别是在那些希望在不影响质量的情况下,将交付时间从月/年减少到日/周的(软件)公司中。除了其他模式和技术外,这还导致了基于微服务的架构的采用。


在微服务架构中,许多服务可能同时演化,而且通常非常迅速。然而,更重要的是,它们必须以一种孤立的方式单独发布,这就意味着发布不是在服务之间协调进行的。


因此,如果你采用微服务架构(包括它所包含的所有内容),那么你每天可以发布多次,但是这又带来了另一个问题:很难验证新服务(或服务的新版本)会不会破坏当前应用程序中的任何内容。


让我们看一个示例,你可能会因为一个服务的更新而中断另一个服务。

微服务发布编排面临的挑战

假设我们有一个消费者服务 A(v1)和一个提供者服务 B(v1)。服务 B(v1)提供一个 JSON 文档作为输出,其中一个字段名为 name,服务 A (v1)使用该字段。


现在,创建一个服务 B(v2),它将字段从 name 改为 fullname。然后,你修复服务 B(v2)的所有测试,使它们不会因为这个修改而失败。因为理论上,任何服务都可以独立发布,你将这个新版本部署到生产环境,当然,服务 B(v2)的行为没有问题,但服务(v1)将会立即开始失败,因为它没有获得预期的数据(例如,服务 A 希望得到字段 name 却接收 fullname)。



所以你可以看到,单元测试(在这里是服务 B)和一般测试可以帮助获得信心,相信我们正在做的事情是对的,但这并不涵盖整个系统的总体逻辑(即我们无意中破坏了依赖 B 的服务 A)。

一种潜在的解决方案:引入分接比较

“分接比较(Tap compare)”是一种测试技术,它允许你将新服务的结果与旧服务进行比较,从而测试新服务的行为/性能。


它被用来检测不同类型的回归,例如,请求/响应格式回归(新服务破坏了与消费者的向后兼容性)、性能回归(新服务表现低于旧服务),或者仅仅是代码缺陷(通过比较两个服务的响应)。


分接比较方法不需要开发人员创建复杂的测试脚本,其他类型的测试通常需要,如集成测试或端到端测试。在分接比较方法中,你可以使用镜像流量技术或捕获(跟踪)部分公共流量,并在服务的新版本上重放。这些技术超出了这篇文章的范围,简单起见,作为分接比较技术的入门指南,我们通过一个测试“模拟”镜像流量的方法。

为什么是分接比较?

分接比较并不是要直接代替任何其他测试技术——你仍然需要编写其他类型的测试,如单元测试、组件测试或契约测试。不过,它可以帮助你发现回归,这样你对开发的新版本的服务的质量就更有信心。


但是,分接比较的一个重要特点是,它为你的服务提供了一个新的质量层。借助单元测试、集成测试和契约测试,作为一名开发人员,你可以根据你对系统的理解进行功能验证,还有你在测试开发过程中所提供的输入和输出。在分接比较测试中,有些完全不同的东西。这里,服务验证使用了生产请求,或者是从生产环境捕获一组请求然后对新服务重放,或者是使用镜像流量技术(克隆)生产流量同时发送给旧版本(生产版本)和新版本,并比较结果。在这两种情况下,作为一个开发者,你都不需要编写测试脚本(提供输入或输出)来进行服务验证——用于验证目的是真实的流量。


分接比较工作在“生产环境”中;你是用生产流量和生产实例来验证同样部署到生产环境中的新服务,因此,你是在生产环境中添加质量检验关,而其他测试技术重点是在部署之前验证软件(单元或组件测试)。

Diferencia

Diferencia 是什么?

Diferencia是一个使用 Go 编写的开源工具(遵循 Apachev2 许可),与 Java JUnit 4、Junit 5 或 AssertJ 这样的框架进行了紧密地集成,让你可以使用分接比较测试验证服务的两种实现的兼容(例如,服务不会破坏交互协议方面的向后兼容性),让我们可以确信变更不会造成回归。


Diferencia 背后的思想是充当代理,收到的每个请求会多路发送给服务的多个版本。当每个服务响应都返回后,比较响应并对它们进行检查,看它们是否“相似”。如果对一定数量的请求重复此操作后,所有(或大多数)的响应都“相似”,那么你可以认为新服务未造成回归。


在下一节中,你会看到为什么我使用“相似”这个词而不是相等。


Diferencia 也可以用 Docker 镜像(lordofthejars/ Diferencia)的形式发布,该镜像基于 Alpine 镜像,可用于 Kubernetes 或 OpenShift 集群。


写这篇文章的时候,Diferencia 的版本是 0.6.0。

Diferencia 的工作机制

Diferencia 充当请求和正在验证的服务的两个版本之间的代理。默认情况下,Diferencia 使用两个不同的服务实例:


  • 现有版本(生产环境中的版本),即主版本;

  • 新版本(发布过程中的版本),即候选版本。


每个请求都以广播的方式发送给两个服务,然后对两个实例的响应进行比较。如果响应相等,则 Diferencia 代理会向调用者返回一个 HTTP 状态码 200 OK。另一方面,如果请求响应不相等,则会向调用者返回一个 HTTP 状态码 412 “前提条件失败”。前提是具有相同参数的相同请求应该产生相同的响应。Diferencia 还在内部存储每个请求的结果,以供稍后查询。



重要的是要注意,Diferencia 并不像一个标准的代理,所以如果不显式设置的话,它返回的不是原始内容。Diferencia 在启动时可以使用镜像流量选项,这使得 Diferencia 可以将来自主要部分的响应重新发送出去。


然而,这只是最简单的情况。当 JSON 文档中的有一些值有本质的不同(或不确定性),例如,一个计数器、一个日期或随机数?尽管响应可能是完全有效的,因为唯一的区别是一个字段的值,两个文档是不相等的,因此就不能保证这种变化是否是回归的原因。


为了避免这个问题(也称为“噪声”),一个自动噪声检测函数会识别包含噪声值的字段,并消除响应中的噪声。这样,噪声值就从比较逻辑中删除了,每个响应在进行比较时就像没有噪声一样了。


要进行自动噪声检测,你需要三个运行的服务实例:


  • 现有版本(生产环境中的版本),称为主版本

  • 现有版本(生产环境中的版本),它是主版本的另一个实例,称为辅助版本

  • 新版本(正在发布过程中的版本),称为候选版本


首先,在比较主版本和候选版本的响应时禁用噪声检测。然后,比较主版本和辅助版本的响应。因为这两个版本是一样的,响应应该是相同的,它们之间的任何差异都被认为是噪声。最后,在比较主版本和候选版本时将噪声移除,就可以确认两个响应彼此相等。



重要的是要注意,在默认情况下,Diferencia 将忽略任何非安全操作,如 POST、PUT、PATCH 等等,因为它们可能对服务产生副作用。可以使用–unsafe 标识禁用此行为。

Diffy 还是 Diferencia

Diferencia 的理念来自另一个名为OpenDiffy的分接比较框架,但它们之间有一些差异。Diferencia 是:


  • 用 Go 编写的,提供容器的轻量级体验;

  • 准备在 Kubernetes 和 OpenShift 集群中使用;

  • 它可以用来镜像流量;

  • 将结果暴露为 Rest API,但也以 Prometheus 格式;

  • 与 Istio 集成;

  • 支持 Postel 定律(后面会详细介绍)。

Diferencia Java

Diferencia-Java 是一个 Diferencia 包装器,它提供了 Java API 让你可以在 Java 中使用它,而不会注意到它是用 Go 实现的。Diferencia-Java 提供了以下特性:


  • Diferencia 可以自动安装,你不需要手动安装任何东西;

  • 在启动/停止 Diferencia 时,你不需要直接和 CLI 打交道;

  • 提供特定的 HttpClient 用于连接 Diferencia Rest API,从而对它进行配置或获取结果;

  • 它可以作为普通的 Java 使用;

  • 与 JUnit4 和 JUnit5 集成;

  • 与 AssertJ 库集成,使测试可读。

Java 示例

在这个例子中,我们使用一种简单的方法,用一个简单的 Rest API 展示 Diferencia 的所有功能。


该服务是使用 MicroProfile 规范开发的,如下所示:


@Path("/user")public class HelloWorldEndpoint {
@GET @Produces("application/json") public Response getUserInformation() { final JsonObject doc = Json.createObjectBuilder() .add("name", "Alex") .build(); return Response.ok(doc.toString()) .build(); }
复制代码


让我们看一下,在这个服务演化为不同版本的过程中如何使用 Diferencia。简单起见,我们设定以下前提:


  • 服务在本地主机上运行;

  • 主服务运行在端口 9090 上;

  • 辅助服务运行在端口 9091 上;

  • 候选服务运行在端口 9092 上。

Java 测试

这个示例使用 JUnit 5 开发测试代码,运行 Diferencia 并检测回归。基本上,这个测试是读取一个文件中指定的 URL 并向 Diferencia 发送请求。最后,如果有回归,它会发出告警。


接下来,依赖项必须包含在类路径中,应该在构建工具中注册:


   <dependency>     <groupId>com.lordofthejars.diferencia</groupId>     <artifactId>diferencia-java-junit5</artifactId>     <version>${version.diferencia}</version>     <scope>test</scope>   </dependency>   <dependency>     <groupId>com.lordofthejars.diferencia</groupId>     <artifactId>diferencia-java-assertj</artifactId>     <version>${version.diferencia}</version>     <scope>test</scope>   </dependency>   <dependency>     <groupId>org.junit.jupiter</groupId>     <artifactId>junit-jupiter-engine</artifactId>     <version>${version.junitJupiter}</version>     <scope>test</scope>   </dependency>   <dependency>     <groupId>org.assertj</groupId>     <artifactId>assertj-core</artifactId>     <version>${version.assertj}</version>     <scope>test</scope>   </dependency>
复制代码


编写一个 JUnit 测试,从一个文件中读取 URL:


@ExtendWith(DiferenciaExtension.class)@DiferenciaCore(primary = "http://localhost:9090", candidate = "http://localhost:9092")public class DiferenciaTest {
private final OkHttpClient client = new OkHttpClient();
@Test public void should_detect_any_possible_regression(Diferencia diferencia) throws IOException { // Given final String diferenciaUrl = diferencia.getDiferenciaUrl();
// When
Files.lines(Paths.get("src/test/resources/links.txt")) .forEach((path) -> sendRequest(diferenciaUrl, path));
// Then
assertThat(diferencia) .hasNoErrors(); }
private void sendRequest(String diferenciaUrl, String path) { final Request request = new Request.Builder() .addHeader("Content-Type", "application/json") .url(diferenciaUrl + path) .build(); try { client.newCall(request).execute(); } catch (IOException e) { throw new IllegalArgumentException(e);
复制代码


当你运行这个测试时,一个*/user 请求会发送到 Diferencia 代理,这是由 JUnit 扩展自己启动的。当 links.txt*文件中定义的所有请求处理完成,就可以断言 Diferencia 代理中没有任何错误,这意味着新服务中没有回归。


因为现在两个服务实例完全相同但运行在不同的端口上,一切顺利。


在更复杂的情况下,这个文件应该是由捕获公共流量生成的,或者只是将公共流量使用镜像技术重定向给 Diferencia 代理。正如上文所言,这超出了本文的范围。


现在,让我们做个修改,把 name 字段改为 fullname,破坏新服务的向后兼容性 。


finalJsonObjectdoc= Json.createObjectBuilder()
.add("fullname", "Alex")
.build();
复制代码


然后,部署这个新版本,再次运行测试,你会发现路径*/user*上有一个回归。


是时候看看噪声检测的作用了。修改现有服务和新服务,让它们包含一个随机数,并再次部署它们。


final JsonObject doc = Json.createObjectBuilder()           .add("name", "Alex")           .add("sequence", new Random().nextInt())           .build();
复制代码


再次运行测试。显然,你会失败,因为 sequence 字段包含一个随机生成的值。


这是一个完美的自动噪声检测用例,所以你需要在端口 9091 上部署一个辅助服务,并让 Diferencia 使用噪声检测。


@DiferenciaCore(primary = "http://localhost:9090", candidate = "http://localhost:9092",   config = @DiferenciaConfig(secondary = "http://localhost:9091", noiseDetection = true))
复制代码


再次运行测试,你将会看到测试通过。自动噪声检测会识别出,sequence 字段的值是噪声,并从比较逻辑中移除。


到目前为止,你已经看到,Diferencia 可用于检测回归,但还有一个重要用例需要提及,就是如何在服务的新版本中正确地重命名字段而不引发回归。

子集模式

要重命名响应中的一个字段,消费者和提供者都应该遵循Postel法则或进行消息序列化和反序列化。Postel 法则(意译)说,“严以律己,宽以待人”。


如果你想把字段 name 重命名为 fullname,你需要先提供这两个字段,这样,就不会对任何消费者造成破坏。


在前面的例子里,新版本的服务应该是下面这个样子:


final JsonObject doc = Json.createObjectBuilder()           .add("name", "Alex")           .add("fullname", "Alex")           .add("sequence", new Random().nextInt())           .build();
复制代码


现在消费者仍兼容新版本,所以没有引入回归…好吧,让我们部署新服务并运行 Diferencia 测试。你会失败,因为主版本和候选版本不相等;新版本有一个旧版本没有的字段。为解决这种假阳性,Diferencia 提供了子集模式。这种模式使 Diferencia 不会失败,它就是为了处理这种情况,即旧版本的响应是新版本的响应子集。


修改测试,使 Diferencia 以子集模式启动。


@DiferenciaCore(primary = "http://localhost:9090", candidate = "http://localhost:9092",   config = @DiferenciaConfig(secondary = "http://localhost:9091", noiseDetection = true, differenceMode = DiferenciaMode.SUBSET))
复制代码


再次运行测试,测试通过,因此,即使在这种情况下,Diferencia 也可以用于检测任何回归问题。

更多特性

在这篇文章中,你已经了解了如何使用 Diferencia Java,但是请记住,Diferencia 是用 Go 编写的,这意味着它可以独立地应用在任何语言中。


此外,Diferencia 还提供了以下特性:


  • HTTPS 支持;

  • 公开结果供 REST API 或 Prometheus 使用;

  • 可视化仪表板;

  • 主版本调用和候选版本调用的平均耗时。

契约测试

分接比较测试不能代替契约测试,但是它们可以充当“监护人”,保证任何未被契约验证测试覆盖的东西(即契约中未指定的操作)不会在新服务发布到生产环境时引入回归。


重要的是要注意,契约测试技术需要大量的技术知识才能有效地实现(特别是在消费者驱动的契约开发的情况下),需要项目的所有团队做出巨大的让步。


在契约测试中,有一个步骤涉及契约的生成,因此,我们还需要自动化这个过程,保持更新或防止任何可能的错误在这个(可能)手动步骤中被引入。

结论

分接比较是一种很好的测试技术,你可以添加到你的工具箱中用于验证服务的新版本没有引入回归,而无需管理和维护一个测试脚本。你可以捕获现有的生产流量并稍后回放,或者使用镜像流量技术克隆请求并同时发送给新版本和旧版本的服务。


在这篇文章中,我重点介绍了 Diferencia 及其与 Java 的集成,但是,它可以作为一个独立的服务,不需要使用 Java(或者任何 JVM 语言)。


如果你想提高应用程序的质量,并添加一个守卫,防止在新版本中出现回归,那么分接比较技术可以为你带来帮助。

关于作者

Alex Soto 是 Red Hat 开发组的软件工程师。他热爱 Java 世界和软件自动化,信任开源软件模型。Alex Soto 是 NoSQLUnit 和 Diferencia 项目的创建者、JSR374 专家组成员(用于 JSON 处理的 Java API)、Testing Java Microservices 一书的作者之一(Manning 出版)以及几个开源项目的贡献者。自 2017 年以来,他成为 Java 冠军和国际演讲者,他介绍新的微服务测试技术和 21 世纪的持续交付。你可以通过 Twitter(@alexsotob)找到他。


查看英文原文Tap Compare Testing with Diferencia and Java Microservices


2019-03-11 08:003896
用户头像

发布了 774 篇内容, 共 526.2 次阅读, 收获喜欢 1578 次。

关注

评论

发布
暂无评论
发现更多内容

Java 并发编程——线程池开篇

Antway

6月日更

证券互动问答平台关键词监控提醒

木头

互动平台 证券监控 股市消息 监控提醒

质量分析工具-监控大厅大揭秘

anyRTC开发者

音视频 WebRTC sdk

构建高可用的MySQL

林一

MySQ MySQL 高可用 Maxscale

Kubernetes 的自动伸缩你用对了吗?

张晓辉

Kubernetes k8s最佳实践

带你剖析鸿蒙轻内核任务栈的源代码

华为云开发者联盟

鸿蒙 任务栈 任务调度 任务上下文

当人工智能遇上视频直播——基于Agora Web SDK实现目标识别

dajyaretakuya

深度学习 音视频 WebRTC 声网 TensorFlow.js

读深入ES6记[二]

蛋先生DX

ES6 6月日更

那个陪我打王者的兄弟进了阿里

艾小仙

理解Linux之文件I/O——知其然,知其所以然

奔着腾讯去

文件管理 Linux内核 文件I/O I/O模型

春色满园关不住,带你体验阿里云 Knative

阿里巴巴云原生

云原生

待办事项列表,敏捷项目管理的核心工件

万事ONES

Scrum 敏捷 研发管理 ONES

缓存穿透、缓存雪崩、缓存击穿问题与优化方案

Skysper

Python——字符串转换与处理

在即

6月日更

Webpack 系列:如何编写loader

范文杰

webpack 6月日更

Packer 自动化镜像 Windows 安装过程

HoneyMoose

模块六作业

c

架构实战营

想做DBA,多租户管理你一定要知道这些

华为云开发者联盟

多租户 GaussDB(DWS) 资源池 存储空间 资源隔离

内嵌双向链表的设计与实现

实力程序员

聊聊追求测试技术导致过度测试

陈磊@Criss

一文教会你认识Vuex状态机

华为云开发者联盟

Vue 应用 vuex 事件 父子组件

【LeetCode】从上到下打印二叉树 Java题解

Albert

算法 LeetCode 6月日更

SpringBootApplication注解

梦倚栏杆

【Flutter 专题】109 图解自定义 ACERadio 单选框

阿策小和尚

Flutter 小菜 0 基础学习 Flutter Android 小菜鸟 6月日更

Locust完成gRPC协议的性能测试

陈磊@Criss

密码学系列之:生日攻击

程序那些事

加密解密 密码学 程序那些事

企业应用AI之路怎么走?飞桨实践有真知

百度大脑

AI 飞桨

递归全排列问题(两种方法 Java实现)

若尘

数据结构 递归 6月日更

如何进行可视化大屏视觉设计?

博文视点Broadview

☕️【Java技术之旅】站在Linux操作系统角度去看Thread(线程)

码界西柚

线程 Thread 6月日更 内核线程

小白必看的,JS中循环语句大集合

华为云开发者联盟

JavaScript js 循环语句 while循环 for循环

利用Diferencia和Java微服务进行分接比较测试_语言 & 开发_InfoQ精选文章