写点什么

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

作者:Alex Soto

2019 年 3 月 11 日

利用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 年 3 月 11 日 08:003363
用户头像

发布了 362 篇内容, 共 158.1 次阅读, 收获喜欢 814 次。

关注

评论

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

MyBatis标签trim,你不会以为我是去空格的吧?

Java小咖秀

Java mybatis Java 面试

一个典型的大型互联网应用系统使用了哪些技术方案和手段,主要解决什么问题?

朱月俊

谈反应式编程在服务端中的应用,数据库操作优化,提速 Upsert

newbe36524

C# MySQL 数据库 mongodb Reactive

GO语言泛型编程实践

老胡爱分享

go 泛型

清华百万年薪架构师,精心编写多线程与高并发实战PDF

互联网架构师小马

Java 程序员 多线程 架构师 多线程与高并发

区块链冷链食品追溯系统

CECBC区块链专委会

区块链技术 上链 溯源 浙冷链

《架构师训练营》第四周总结

央行数字货币:第三方支付产业新变量

CECBC区块链专委会

数字货币 DCEP 区块链技术

关于编码的一点“思考”

damnever

golang 思考 抽象 分层架构 编码

小师妹学JVM之:JIT中的PrintAssembly

程序那些事

JVM 小师妹 性能调优 JIT 汇编

互联网系统常见问题以及解决方案

而立

极客大学架构师训练营

原来使用Postman如此简单,API测试之Postman使用全指南

软测小生

接口 Postman 接口测试 API API测试

消息队列(三)如何保证消息不被重复消费?

奈何花开

Java MQ 消息队列

CECBC带你一图看懂区块链

CECBC区块链专委会

CECBC 区块链技术 去中心化

《架构师训练营》第四周命题作业

python中对字典与列表组合进行排序

开心太平洋

Python List 排序

ARTS打卡 第5周

引花眠

ARTS 打卡计划

ARTS-WEEK5

一周思进

ARTS 打卡计划

架构师训练营第四周学习总结

CATTY

快来解锁Pepper机器人新技能,够酷Pepper就跟你回家!

阿甜

编程 开发者 App 开发 机器人

【源码系列】Spring Cloud Eureka

Alex🐒

源码 Spring Cloud Eureka

太厉害了!阿里年薪120W架构师整理的学习笔记,看完收获良多

互联网架构师小马

Java 学习 阿里巴巴 程序员 架构师

消息队列(二)如何保证消息队列的高可用?

奈何花开

Java MQ 消息队列

信息的表示与存储-浮点数的运算

引花眠

计算机基础

一文带你学会 Blob(含 7 个使用场景)

pingan8787

Java 前端 Web Blob

架构师训练营作业 -Week4

wyzwlj

极客大学架构师训练营

架构师训练营 - 学习笔记 - 第四周

心在飞

极客大学架构师训练营

SQL运行内幕:从执行原理看调优的本质

arthinking

MySQL 数据库

阿里待遇那么好,你为什么从阿里离职?

互联网架构师小马

Java 阿里巴巴 程序员 找工作 离职

学习总结 - 第 4 周

饶军

系统架构感想

朱月俊

Leader修炼指“北”:管理路上的大小Boss

Leader修炼指“北”:管理路上的大小Boss

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