写点什么

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

发布了 742 篇内容, 共 482.5 次阅读, 收获喜欢 1549 次。

关注

评论

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

使用ABAP(ADBC)和Java(JDBC)连接SAP HANA数据库

汪子熙

JavaScript SAP abap WebClient UI SAP UI5

乾坤大挪移:SAP CRM WebClient UI 和 SAP Fiori UI 混搭并存

汪子熙

CRM SAP abap WebClient UI bsp

Linux网络编程-UDP和TCP协议详解

Linux服务器开发

TCP 网络编程 udp 网络协议栈 Linux服务器开发

IP地址定位在网站上的几个代表性应用

郑州埃文科技

Hybris ECP里Customer对应的数据库表

汪子熙

JavaScript CRM SAP WebClient UI SAP UI5

help.hybris.com和help.sap.com网站的搜索实现

汪子熙

Java SAP abap Hybris

Hybris UI的Route(路由)实现

汪子熙

Java SAP WebClient UI Hybris

Jerry和您聊聊Chrome开发者工具

汪子熙

chrome 大前端 SAP Chrome开发者工具

Redis - 复制

旺仔大菜包

redis

Golang Testing 概览 - 深入篇

hedzr

testing Go 语言

直呼内行!阿里大佬离职带出内网专属“高并发系统设计”学习笔记

Java 程序员 架构 面试

如何查看SAP CRM WebUI,C4C和Hybris里的页面技术信息

汪子熙

CRM abap WebClient UI SAP UI5

Pulumi AWS 在进行预览更新的时候持续提示 Key 错误

HoneyMoose

宏碁亮相2021西洽会,以绿色智能“洽谈未来”

科技热闻

Golang Profiling: 关于 pprof

hedzr

Go 语言 profiling

JavaScript and Ruby in ABAP

汪子熙

JavaScript SAP abap WebClient UI

Internationalization(i18n) support in SAP CRM,UI5 and Hybris

汪子熙

JavaScript CRM SAP abap SAP UI5

使用ABAP批量下载Markdown源文件里的图片到本地

汪子熙

markdown SAP abap download

Golang Testing 概览 - 补充篇

hedzr

testing Go 语言 assertion

戏说代理模式

编程三昧

随笔 设计模式 开发 代理模式

从天而降的AI“青云梯”,开发者们准备好了吗?

脑极体

微博评论高性能高可用计算架构设计

Lane

区块链与物联网的强强联合将带来巨变

CECBC

SAP Cloud for Customer(C4C)和微软Outlook的集成

汪子熙

微软 SAP abap SAP UI5 outlook

SAP ABAP和Hybris的源代码生成工具

汪子熙

Java SAP abap Hybris commerce

你知道 Redis 可以实现延迟队列吗?

xcbeyond

队列 延迟队列 6月日更

还在用SELECT COUNT统计数据库表的行数?Out了

汪子熙

SAP abap hana

膜拜!首次公布Java10W字面经,Github访问量破百万

Java 程序员 架构 面试

C/C++学习:C++并发与多线程

奔着腾讯去

c++ 并发 多线程并发 POSIX线程 C++11线程

ABAP下载的病毒扫描Virus Scan

汪子熙

下载 SAP abap 病毒扫描

通过ABAP代码判断当前系统类型,BYD还是S4 OP还是S4 Cloud

汪子熙

SAP abap S/4HANA SAP Business ByDesign

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