重要要点
- 在微服务架构中,许多服务可能同时(相对)隔离地发展,并且通常非常Swift。 为了获得这种体系结构样式的全部价值,服务必须能够独立发布。
- 通常很难验证新服务(或服务的新版本)是否不会破坏当前应用程序中的任何内容,即通过更改API,有效负载或响应性能而导致性能下降。
- “点击比较”是一种测试技术,可让您通过将新服务的结果与旧服务进行比较来测试新服务的行为和性能。 本文提供了一个在新旧服务之间镜像生产流量的示例,并比较了结果差异。
- Diferencia是一种用Go语言编写的开源工具(根据Apache License v2发行),并且与Java测试框架(如JUnit 4,Junit 5或AssertJ)紧密集成,可让您使用抽头比较测试技术来验证服务的两个实现。在语法上兼容。
在过去的几年中,DevOps的受欢迎程度大大提高,尤其是在(软件)公司中,他们希望在不影响质量的前提下缩短以天/周而不是数月/年为单位的交货时间。 除其他模式和技术外,这导致采用基于微服务的体系结构。
在微服务体系结构中,很多服务可能同时(而且通常是非常快地)在发展。 但是,更重要的是,它们必须以隔离的方式独立释放,这实际上意味着不会在服务之间精心设计释放。
因此,如果您接受微服务架构(包含其所有含义),则可以每天释放几次,但这又引发了另一个问题:很难验证新服务(或服务的新版本)是否不会中断当前应用程序中的任何内容。
让我们看一个示例,其中由于另一个服务的更新而可能中断服务。
编排微服务版本面临的挑战
假设我们有一个服务A(v1),也称为使用者,并且有服务B(v1),也称为提供者。 服务B(v1)提供了一个JSON文档作为输出,该文档包含一个名为name的字段,该字段由服务A(v1)使用和使用。
现在,您创建一个服务B(v2),该服务将字段从名称更改为全名。 然后,修复服务B(v2)的所有测试,以确保它们不会由于此修改而失败。 因为从理论上讲,任何服务都可以独立发布,所以您可以将此新版本部署到生产环境中,当然,服务B(v2)将正常运行,但是服务A(v1)将立即开始失败,因为它没有获取数据这是预期的(例如,服务A需要一个字段名称,但接收到字段全名)。
因此,您可以看到,单元测试(在这里是服务B的情况)和测试通常有助于使我们对我们正在正确地做事的信心,但这并不涵盖整个系统的总体逻辑(即,无意中打破了依赖服务A)。
潜在的解决方案:引入分路比较
“点击比较”是一种测试技术,可让您通过将新服务的结果与旧服务进行比较来测试新服务的行为/性能。
它用于检测不同类型的回归,例如,请求/响应格式回归(一项新服务正在破坏与一个使用者的向后兼容性),性能退化(一项新服务的运行速度比旧服务慢)或仅通过比较两种服务的响应。
抽头比较方法不需要开发人员创建复杂的测试脚本,就像其他类型的测试(例如集成测试或端到端测试)通常那样。 在点击比较中,您可以使用镜像流量技术或捕获(阴影)一部分公共流量,然后针对新版本的服务重播此流量。 这些技术不在本文的讨论范围之内,为简单起见,作为抽头比较技术的入门指南,我们将通过测试“模拟”镜像流量方法。
为什么点击比较?
点击比较并不是试图直接替代任何其他测试技术的东西-您仍然需要编写其他类型的测试,例如单元测试,组件测试或合同测试。 但是,它可以帮助您检测回归,从而使您对开发的服务的新版本的质量更有信心。
但是,点击比较的重要一件事是,它为您的服务提供了新的质量层。 通过单元测试,集成测试和合同测试,这些测试会根据您(作为开发人员)对系统的了解来验证功能,因此,您可以在测试开发期间提供输入和输出。 在抽头比较的情况下,这是完全不同的。 在这里,服务的验证与生产请求一起发生,方法是从生产环境中捕获一组请求,然后针对新服务重播它们,或者使用镜像流量技术,在其中转移(克隆)要发送到的生产流量无论是旧版本(生产版本)还是新版本,您都可以比较结果。 在这两种情况下,作为开发人员,您都无需编写用于验证服务的测试脚本(提供输入或输出),这是用于验证目的的实际流量。
在“生产环境”内点击比较作品; 您正在使用生产流量和生产实例来验证也已部署到生产环境的新服务,因此您要在生产环境中添加质量门,而其他测试技术则侧重于在部署软件之前验证软件的正确性(即单元或组件测试)。
Diferencia
什么是Diferencia?
Diferencia是一个用Go语言编写的开源工具(根据Apache License v2发行),并与Java紧密集成在JUnit 4,Junit 5或AssertJ等框架中,该工具可让您使用抽头比较测试技术来验证服务的两个实现是兼容(例如,服务不会破坏与交互协议有关的向后兼容性),并增加对更改无回归的信心。
Diferencia背后的想法是充当代理,将接收到的每个请求都多播到正在运行的服务的多个版本中。 当每个服务的响应返回时,它会比较响应并检查它们是否“相似”。 如果在重复执行具有代表性数量的不同请求的操作后,所有(或大多数)请求都是“相似的”,则可以认为您的新服务是无回归的。
在下一节中,您将看到为什么我使用术语“相似”而不是相等。
Diferencia还作为基于Alpine映像的Docker映像(lordofthejars / diferencia)交付,可以在Kubernetes或OpenShift集群中使用。
在撰写本文时,Diferencia的版本为0.6.0。
这个怎么运作
Diferencia充当请求和要验证的服务的两个版本之间的代理。 默认情况下,Diferencia使用两种不同的服务实例:
- 现有版本(生产中的版本)称为主要版本。
- 新版本(正在发布的版本)称为候选版本。
每个请求都广播给它们两个,并比较两个实例的响应。 如果响应相等,则Diferencia代理向调用方返回HTTP状态代码200 OK。 另一方面,如果请求不相等,则将HTTP状态代码412“前提条件失败”发送回调用方。 前提是具有相同参数的相同请求应产生相同的响应。 内部Diferencia还存储每个请求的结果,以便以后可以查询。
重要的是要注意,Diferencia的行为不像标准代理,因此,如果未明确设置服务的原始内容,则不会返回该原始内容。 可以在“镜像流量”选项中启动Diferencia,该选项使Diferencia可以发回来自主要元素的响应。
但是,这只是最简单的情况。 当JSON文档中存在一些本质上不同(或不确定)的值(例如计数器,日期或随机数)时,会发生什么? 尽管两个响应都可能完全正确,但是由于唯一的区别在于字段的值,所以两个文档都不相等,因此不能保证此更改是否归因于回归。
为避免此问题(也称为“噪声”),自动噪声检测功能将包含噪声的字段标识为值,并从响应中删除该噪声。 这样,可以从比较逻辑中去除噪声值,并且可以像没有噪声一样对每个响应进行比较。
要具有自动噪音检测功能,您需要三个正在运行的服务实例:
- 现有版本(生产中的版本)称为primary 。
- 现有版本(生产中的版本),它是primary的另一个实例,称为secondary 。
- 被称为候选版本的新版本(正在发布的版本)。
首先,比较主要响应和候选响应,因为它禁用了噪声检测。 然后,还比较了主要和次要的响应。 由于两个版本相同,因此响应应该相同,并且它们之间的任何差异都被视为噪声。 最终,从主要和候选者之间的比较中去除了噪声,并验证了两个响应彼此相等。
重要的是要注意,默认情况下,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,您不需要手动安装任何东西。
- 在不直接处理CLI的情况下启动/停止Diferencia。
- 特定的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和检测回归。 基本上,此测试将针对Diferencia答复文件中指定的URL列表。 最后,它断言是否存在回归。
接下来,依赖项必须在您的类路径中,并且应该在构建工具中注册:
<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>
并编写一个从文件读取URL的JUnit测试:
@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();
再次运行测试。 显然,您会失败,因为序列字段包含随机生成的值。
这是自动噪声检测的理想用例,因此您需要在端口9091上部署辅助服务,并使Diferencia能够使用噪声检测。
@DiferenciaCore(primary = "http://localhost:9090", candidate = "http://localhost:9092",
config = @DiferenciaConfig(secondary = "http://localhost:9091", noiseDetection = true))
再次运行测试,您将看到绿色的条。 自动噪声检测可识别出序列字段的值是噪声,并将其从比较逻辑中删除。
到目前为止,您已经看到Diferencia可用于检测回归,但是仍然有一个重要的用例可以解决,这就是如何在新版本的服务中正确实现字段重命名而又不触发回归的方法。
子集模式
要重命名响应中的字段,消费者和提供者都应遵守Postel的法律或对消息进行序列化和反序列化。 Postel的法律说(措辞):“在您发送的内容上保持保守,在您所接受的内容上保持自由”。
如果要将字段名称重命名为全名,则需要首先提供两个字段,这样才不会破坏任何使用者。
在前面的示例中,服务的新版本应如下所示:
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也可以用于检测任何回归问题。
更多功能
在本文中,您学习了如何在Java中使用Diferencia,但是请记住Diferencia是用Go编写的,这意味着它可以在任何语言中独立使用。
此外,Diferencia还为您提供以下功能:
- HTTPS支持。
- 公开要由REST API和/或Prometheus使用的结果。
- 可视仪表板。
- 主电话和候选电话中经过的平均时间。
合同测试
抽头比较测试不能替代合同测试 ,但它们可以充当“监护人”,因此,合同验证测试中未涵盖的任何内容(即合同中未指定的操作)在以下情况下均无法引入回归:新服务投入生产。
重要的是要注意,合同测试是一项需要大量技术知识才能有效实施的技术(尤其是在以消费者为主导的合同开发情况下),并且项目的所有团队都必须在很大程度上损害其能力。技术。
在合同测试中,有一个涉及合同生成的步骤,因此我们还需要一个流程来使其自动化,保持最新,或防止在此(可能)手动步骤中引入任何可能的错误。 。
结论
点击比较是一种很好的测试技术,可以添加到您的工具箱中,以验证新版本的服务上是否没有回归,而无需整理和维护测试脚本。 您可以捕获现有的生产流量并在以后重播,或者使用镜像流量技术克隆请求并将其发送到新旧服务版本。
在本文中,我重点介绍了Diferencia及其与Java的集成,但是它可以用作独立服务,并且不需要使用Java(或任何JVM语言)。
如果您想提高应用程序的质量并添加保护措施以防止新版本中的性能下降,那么“ Tap比较”是一种可以帮助您的技术。