AICon 上海站|90%日程已就绪,解锁Al未来! 了解详情
写点什么

利用 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:003909
用户头像

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

关注

评论

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

不同设备如何统一语言编程平台高效开发?本文为你揭秘

HarmonyOS开发者

HarmonyOS

阿里Dubbo技术负责人准备的源码教程,很有大厂风格

做梦都在改BUG

Java dubbo

代码注释的艺术,优秀代码真的不需要注释吗?

阿里技术

代码注释

精准测试之过程与实践 | 京东云技术团队

京东科技开发者

精准测试 质量保障 企业号 5 月 PK 榜

已有多人中招:这张特殊二维码可致微信闪退,还会进入安全模式

Rose

微信防撤回 微信下载 微信小助手 微信闪退

需求梳理会开2天是否合理?

BY林子

软件质量 需求分析 需求工程师 需求梳理

中国开源未来发展峰会“问道 AI 分论坛”即将开幕!

kk-OSC

峰会

从零开始打造一款基于SpringBoot+SpringCloud的后台权限管理系统

做梦都在改BUG

Java Spring Cloud Spring Boot 权限管理

最具有中国特色的微服务组件!阿里新一代SpringCloud学习指南

做梦都在改BUG

Java 架构 微服务 Spring Cloud spring cloud alibaba

一顿饭的事儿,搞懂了Linux5种IO模型

Java你猿哥

Java Linux 后端 ssm io

发布会实录|悠易科技CTO李旸:洞察新引擎 品牌新增长

游读分享

被 ChatGPT 点燃的向量数据库们

Bytebase

人工智能 数据库 openai AIGC ChatGPT

苹果Mac电脑安装AutoCAD 2024卡死无响应,怎么办

Rose

cad AutoCAD 2024 Mac版 AutoCAD 2024下载 无响应

适用于Mac的七款最佳高清音乐播放器,专为发烧友而生!

Rose

iTunes mac音乐播放器 苹果系统 音乐软件

好的索引当然是要覆盖了!

江南一点雨

MySQL 数据库

华为研究院19级研究员几年心得终成趣谈网络协议文档,附大牛讲解

做梦都在改BUG

Java 计算机网络 网络协议

Java 网络编程详解

timerring

Java

Omi NTFS磁盘管理 支持在 Mac 上修复和格式化 NTFS 磁盘

Rose

NTFS Disk by Omi NTFS NTFS 磁盘管理器 ntfs

携手共赢 HashData亮相华为合作伙伴大会

酷克数据HashData

Django-Vue-Admin基于django+vue前后端分离开箱即用框架

巨梦科技

django Vue

ThottleStop 软件的应用场景

汪子熙

cpu intel 三周年连更

真香!阿里P8微服务实战心得首次公开,涵盖架构设计所有知识点

Java你猿哥

Java 架构 微服务架构 架构设计 架构师

所谓高手,就是跨过坑和大海| 社区征文

鸿蒙之旅

三周年征文

适用于ARM的Linux系统镜像资源+安装教程

Rose

Linux Mac Parallels Desktop 虚拟机 系统镜像

美团太细了!HashMap可以存null,ConcurrentHashMap不可以为什么

Java你猿哥

Java hashmap ssm

技术领先、“忠”于业务,用友走出多维数据库的价值之路

用友BIP

增强型语言模型——走向通用智能的道路?

百度Geek说

人工智能 机器学习 语言模型 企业号 5 月 PK 榜

2步打通ModelArts和Astro,实现AI应用快速落地

华为云开发者联盟

人工智能 低代码 华为云 华为云开发者联盟 企业号 5 月 PK 榜

ElasticSearch中文分词和模糊查询

北桥苏

php elasticsearch thinkphp

数字人是否能成为企业智能化的门户?

Onegun

人工智能 数字人

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