SpringOne2023解读-01-使用spring-cloud-contract与TestContainer构建可靠程序

个人创作公约:本人声明创作的所有文章皆为自己原创,如果有参考任何文章的地方,会标注出来,如果有疏漏,欢迎大家批判。如果大家发现网上有抄袭本文章的,欢迎举报,并且积极向这个 github 仓库 提交 issue,谢谢支持~
另外,本文为了避免抄袭,会在不影响阅读的情况下,在文章的随机位置放入对于抄袭和洗稿的人的“亲切”的问候。如果是正常读者看到,笔者在这里说声对不起,。如果被抄袭狗或者洗稿狗看到了,希望你能够好好反思,不要再抄袭了,谢谢。

01-使用spring-cloud-contract与TestContainer构建可靠程序

笔者了解了 spring-cloud-contract 的用法之后,尝试了下,目前感觉这个还是太不智能,不推荐使用

  • 原始视频地址:https://www.youtube.com/watch?v=9Mc9Yonj9gs&list=PLgGXSWYM2FpPrAdQor9pi__EV1O69Qbom&index=6
  • 个人翻译地址:https://www.bilibili.com/video/BV1nU421Z7h8/
  • 个人总结代码与介绍地址:https://github.com/HashZhang/SpringOne-2023/tree/main/01-%E4%BD%BF%E7%94%A8spring-cloud-contracts%E4%B8%8ETestContainer%E6%9E%84%E5%BB%BA%E5%8F%AF%E9%9D%A0%E7%A8%8B%E5%BA%8F

我们在协作微服务的时候,可能是不同的人写的,不同的团队写的,不同的语言写的,不同的框架写的。通信方式也千奇百怪,可以通过 http 调用,grpc 调用,或者通过消息队列 kafka 这种异步方式通信。但是,核心其实就是我们之间达成某种约定,约定好数据的格式。这样,我们就需要一种方式,来保证我们的微服务之间的协作即数据格式是可靠的。

这时候,我们就需要使用 spring-cloud-contract 来实现这个功能。spring-cloud-contract 是一个测试框架,它可以帮助我们在开发微服务的时候,通过契约测试来保证微服务之间的协作是可靠的。它的核心思想是,通过契约来定义微服务之间的通信,然后通过测试来保证这个契约是可靠的。

spring-cloud-contract 包含三大块内容:

  • 契约定义:定义微服务之间的通信契约
  • 契约生成:生成契约测试代码
  • 契约测试:通过契约测试来保证微服务之间的通信是可靠的

spring-cloud-contract 使用

主要步骤是:

  1. 编写基类,用于定义测试需要的环境(比如需要 TestContainer 初始化哪些镜像进行使用)
  2. 编写上游信息的代码,来触发契约生成,这个一般需要配合 spring-cloud-contract-samples( https://github.com/spring-cloud-samples/spring-cloud-contract-samples ),复制里面的代码模拟你的环境
  3. 编写契约定义
  4. 进行契约测试,自动生成契约测试代码

这里以他们的示例,演示下上面的步骤,他们的代码主要是一个咖啡服务,咖啡师通过 kafka 接收订单信息,然后制作咖啡,然后通过 kafka 发送制作好的咖啡信息,或者如果订单中的咖啡没有,就发送错误消息到 kafka。

首先编写测试基类,通过 TestContainer 初始化 kafka 镜像:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = {BaristaApplication.class, BaseTestClass.TestConfig.class})
@Testcontainers
@AutoConfigureMessageVerifier
@ActiveProfiles("contracts")
public abstract class BaseTestClass {@AutowiredKafkaHandler kafkaHandler;@Container@ServiceConnectionstatic KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka"));
}

然后,参考 spring-cloud-contract-samples( https://github.com/spring-cloud-samples/spring-cloud-contract-samples ),复制其中的 MessageVerifierReceiver 代码,并且配置我们自己的超时时间:

    static class KafkaMessageVerifier implements MessageVerifierReceiver<Message<?>> {private static final Log LOG = LogFactory.getLog(KafkaMessageVerifier.class);Map<String, BlockingQueue<Message<?>>> broker = new ConcurrentHashMap<>();@Overridepublic Message receive(String destination, long timeout, TimeUnit timeUnit, @Nullable YamlContract contract) {broker.putIfAbsent(destination, new ArrayBlockingQueue<>(1));BlockingQueue<Message<?>> messageQueue = broker.get(destination);Message<?> message;try {message = messageQueue.poll(timeout, timeUnit);} catch (InterruptedException e) {throw new RuntimeException(e);}if (message != null) {LOG.info("Removed a message from a topic [" + destination + "]");LOG.info(message.getPayload().toString());}return message;}@KafkaListener(id = "baristaContractTestListener", topics = {"errors", "servings"})public void listen(ConsumerRecord payload, @Header(KafkaHeaders.RECEIVED_TOPIC) String topic) {LOG.info("Got a message from a topic [" + topic + "]");Map<String, Object> headers = new HashMap<>();new DefaultKafkaHeaderMapper().toHeaders(payload.headers(), headers);broker.putIfAbsent(topic, new ArrayBlockingQueue<>(1));BlockingQueue<Message<?>> messageQueue = broker.get(topic);messageQueue.add(MessageBuilder.createMessage(payload.value(), new MessageHeaders(headers)));}@Overridepublic Message receive(String destination, YamlContract contract) {return receive(destination, 15, TimeUnit.SECONDS, contract);}}

然后,编写契约定义(可以用 groovy,也可以用 yaml,不推荐用 java):

package contractsimport org.springframework.cloud.contract.spec.Contract/*** @author Olga Maciaszek-Sharma*/Contract.make {label("serving")input {triggeredBy("triggerServing()")}outputMessage {sentTo("servings")body([beverages:[[uuid  : $(anyUuid()),coffee: [name         : "V60",coffeeContent: "500",device       : "V60"]],[uuid  : $(anyUuid()),coffee: [name              : "Latte",coffeeContent     : "60",steamedMilkContent: "180",milkFoamContent   : "5"]]]])headers {messagingContentType(applicationJson())header 'testKey1', 'testValue1'}}}

这里的契约定义,就是定义了一个触发条件,然后定义了输出的消息内容(可以用很多方便的方法,例如 anyUuid(),anyInteger(),anyString() 等等)。触发条件是 triggerServing(),我们需要编写这个触发条件的代码,然后,spring-cloud-contract 会自动生成契约测试代码:

public void triggerServing() {Order order = new Order();order.add(new OrderEntry("latte", 1));order.add(new OrderEntry("v60", 2));//kafka 发送 order 消息kafkaHandler.process(order);
}

之后,运行 mvn clean test,spring-cloud-contract 会自动生成契约测试代码并运行测试,生成的测试代码在 target/generated-test-sources/contracts 目录下,样子是:

    @Testpublic void validate_shouldSendServing() throws Exception {// when:triggerServing();// then:ContractVerifierMessage response = contractVerifierMessaging.receive("servings",contract(this, "shouldSendServing.yml"));assertThat(response).isNotNull();// and:assertThat(response.getHeader("contentType")).isNotNull();assertThat(response.getHeader("contentType").toString()).isEqualTo("application/json");assertThat(response.getHeader("testKey1")).isNotNull();assertThat(response.getHeader("testKey1").toString()).isEqualTo("testValue1");// and:DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload()));assertThatJson(parsedJson).array("['beverages']").contains("['uuid']").matches("[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}");assertThatJson(parsedJson).array("['beverages']").field("['coffee']").field("['name']").isEqualTo("V60");assertThatJson(parsedJson).array("['beverages']").field("['coffee']").field("['coffeeContent']").isEqualTo("500");assertThatJson(parsedJson).array("['beverages']").field("['coffee']").field("['device']").isEqualTo("V60");assertThatJson(parsedJson).array("['beverages']").field("['coffee']").field("['name']").isEqualTo("Latte");assertThatJson(parsedJson).array("['beverages']").field("['coffee']").field("['coffeeContent']").isEqualTo("60");assertThatJson(parsedJson).array("['beverages']").field("['coffee']").field("['steamedMilkContent']").isEqualTo("180");assertThatJson(parsedJson).array("['beverages']").field("['coffee']").field("['milkFoamContent']").isEqualTo("5");}

通过 mvn clean deploy,我们可以将契约测试代码部署到我们的 CI/CD 环境中,这样,其他人就可以使用我们的契约测试代码来保证他们的微服务是可靠的。

笔者为何不推荐使用

但是,笔者不推荐使用 spring-cloud-contract 的原因:

  1. spring-cloud-contract 需要配合 spring-cloud-contract-samples( https://github.com/spring-cloud-samples/spring-cloud-contract-samples ) 这个项目使用,根据自己的场景需要复制对应的代码去模拟对应的场景。
  2. 需要手动编写 contracts 约定,同时,如果 contracts 更新,需要手动更新对应的测试代码(即每次都要 mvn clean 重新生成)。
  3. 同时,测试代码的可读性大大降低,学习成本很高。

虽然 spring-cloud-contract 有很多的优点,但是笔者认为,它的缺点更多,还需要很长的路要走。可以保持关注。

Spring Boot 与 TestContainer 的集成改进

可以参考这篇文章:https://spring.io/blog/2023/06/23/improved-testcontainers-support-in-spring-boot-3-1
详细的有关 TestContainer 的介绍可以参考我的系列文章:深入理解并应用TestContainer系列

其实就是我们在开发过程中,可能也需要用到 TestContainer 来本地启动我们的项目,但是把 TestContainer 加入非 test 的依赖(例如maven 的 dependency 的 scope 为 test,这样打包的时候不会打进去需要这个依赖),可能会导致我们的项目打包臃肿。 所以,spring-boot 3.1 提供了一个新的特性,我们可以在单元测试中添加一个新的 Main 类。

@SpringBootApplication
public class LocalTestMain {public static void main(String[] args) {SpringApplication.from(Main::main).with(TestContainerConfig.class).run(args);}@TestConfiguration(proxyBeanMethods = false)static class TestContainerConfig {@Bean@ServiceConnectionpublic MySQLContainer<?> mysqlContainer() {return new MySQLContainer<>("mysql");}}
}

这里是使用了一个新的方法 SpringApplication.from, MyApplication 是你原来的 Spring Boot 应用入口类,这里的意思是从原来的入口类启动。有了这个方法,我们可以在启动的时候加入一个包含 MySQLContainer 的 TestContainer 配置的类。

其实,这里的 @ServiceConnection 是一个自定义的注解,就是起到了之前下面这段代码的作用(@Container自动在合适的时候调用 start 方法启动容器,并且在测试结束时关闭容器,@DynamicPropertySource 在容器启动后,将容器的属性塞入 spring 对应属性来兼容测试):

@Container
static MySQLContainer mySQLContainer = new MySQLContainer("mysql");@DynamicPropertySource
static void setProperties(DynamicPropertyRegistry registry) {registry.add("spring.datasource.url", mySQLContainer::getJdbcUrl);registry.add("spring.datasource.username", mySQLContainer::getUsername);registry.add("spring.datasource.password", mySQLContainer::getPassword);
}

目前,这个特性支持的 TestContainer 的容器类型有限,参考:https://spring.io/blog/2023/06/23/improved-testcontainers-support-in-spring-boot-3-1 ,其实也很好理解,因为要自动填充 spring 的属性,必须是 spring 封装了客户端的容器,所以目前支持的容器有:

  • CassandraContainer
  • CouchbaseContainer
  • ElasticsearchContainer
  • GenericContainer(使用的镜像是:redis 或者 openzipkin/zipkin)
  • JdbcDatabaseContainer
  • KafkaContainer
  • MongoDBContainer
  • MariaDBContainer
  • MSSQLServerContainer
  • MySQLContainer
  • Neo4jContainer
  • OracleContainer
  • PostgreSQLContainer
  • RabbitMQContainer
  • RedpandaContainer

微信搜索“hashcon”关注公众号,加作者微信
image
我会经常发一些很好的各种框架的官方社区的新闻视频资料并加上个人翻译字幕到如下地址(也包括上面的公众号),欢迎关注:

  • 知乎:https://www.zhihu.com/people/zhxhash
  • B 站:https://space.bilibili.com/31359187

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/2775171.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

Mac使用AccessClient打开Linux堡垒机跳转闪退问题解决

登录公司的服务器需要使用到堡垒机&#xff0c;但是mac使用AccessClient登录会出现问题 最基础的AccessClient配置 AccessClient启动需要设置目录权限&#xff0c;可以直接设置为 权限 777 chmod 777 /Applications/AccessClient.app注: 如果不是这个路径,可以打开终端,将访达中…

day06.C++排序(整理)

一.直接插入排序 void Insertsort(int *a,int n){int i,j;for( i1;i<n;i){if(a[i]<a[i-1]){int tempa[i];//哨兵for( ji-1;temp<a[j];j--){a[j1]a[j];//记录后移}a[j1]temp;//插入到正确位置}} }二.希尔排序 void Shellsort(int *a,int n){for(int dltan/2;dlta>…

多元回归分析:理论与应用

多元回归分析是一种统计方法&#xff0c;用于研究两个或多个自变量&#xff08;解释变量&#xff09;与一个因变量&#xff08;响应变量&#xff09;之间的关系。这种分析允许研究者评估多个因素对结果变量的影响&#xff0c;是社会科学、经济学、生物医学和工程等多个领域中常…

26、Makefile/shell/字符串处理相关练习20240208

一、现有文件test.c\test1.c\main.c , 请编写Makefile. vi Makefile 创建 代码&#xff1a; CCgcc EXEwho OBJS$(patsubst %.c,%.o,$(wildcard *.c)) FLAGS-c -oall:$(EXE)$(EXE):$(OBJS)$(CC) $^ -o $%.o:%.c$(CC) $(FLAGS) $ $^.PHONY:cleanclean:rm $(OBJS) $(EXE)运行&a…

网络分析仪的防护技巧

VNA的一些使用防护技巧&#xff0c;虽不全面&#xff0c;但非常实用&#xff1a; [1] 一定要使用正规接地的三相交流电源线缆进行供电&#xff0c;地线不可悬浮&#xff0c;并且&#xff0c;火线和零线不可反接&#xff1b; [2] 交流供电必须稳定&#xff0c;如220V供电&#x…

Makefile编译原理 make的隐性规则

一.makefile 中的同名目标 下面程序怎么执行&#xff1f;为什么&#xff1f; 实验1 &#xff1a;makefile 中出现同名目标时 .PHONY : all all : echo "command-1"all : echo "command-2"VAR : testall :echo "all : $(VAR)"mhrubuntu:~/work/…

微软.NET6开发的C#特性——委托和事件

我是荔园微风&#xff0c;作为一名在IT界整整25年的老兵&#xff0c;看到不少初学者在学习编程语言的过程中如此的痛苦&#xff0c;我决定做点什么&#xff0c;下面我就重点讲讲微软.NET6开发人员需要知道的C#特性&#xff0c;然后比较其他各种语言进行认识。 C#经历了多年发展…

回归预测 | Matlab实现WOA-CNN-LSTM-Attention鲸鱼算法优化卷积长短期记忆网络注意力多变量回归预测(SE注意力机制)

回归预测 | Matlab实现WOA-CNN-LSTM-Attention鲸鱼算法优化卷积长短期记忆网络注意力多变量回归预测&#xff08;SE注意力机制&#xff09; 目录 回归预测 | Matlab实现WOA-CNN-LSTM-Attention鲸鱼算法优化卷积长短期记忆网络注意力多变量回归预测&#xff08;SE注意力机制&…

Java_栈_队列

文章目录 一、栈&#xff08;Stack&#xff09;1.概念2.栈的使用3.栈的模拟实现1、定义接口2、定义栈3、成员4、构造方法5、判断空间是否满 full6、入栈 push7、出栈 pop8、获取栈顶元素 peek9、获取栈中有效元素个数 size10、检测栈是否为空 empty完整代码 4.练习1、有效括号2…

单片机的省电模式及策略

目录 一、单片机省电的核心策略 二、单片机IO口的几种模式 三、单片机的掉电运行模式 &#xff08;1&#xff09; 浅谈cpu运行为什么会需要时钟&#xff1f; &#xff08;2&#xff09;STC15系列单片机内部可以配置时钟 &#xff08;3&#xff09;分频策略&#xff0c;降低…

车载自动化项目:Python

1. 自动化测试用的什么框架&#xff1f; 第一种&#xff1a;PythonSeleniumuittest框架 首先是拿到需求文档&#xff0c;基于这个需求去进行搭建。 用pytestrequestallure 这些第三方库进行编写自动化脚本。 举个例子一般的话整个的一个自动化的搭建是分为6层嘛&#xff1a…

Java学习笔记2024/2/8

面向对象 //面向对象介绍 //面向: 拿、找 //对象: 能干活的东西 //面向对象编程: 拿东西过来做对应的事情 //01-如何设计对象并使用 //1.类和对象 //2.类的几个不错注意事项 1. 类和对象 1.1 类和对象的理解 客观存在的事物皆为对象 &#xff0c;所以我们也常常说万物皆对…

redhat grub.cfg配置文件丢失或报错解决

1.实验环境&#xff1a;把grub.cfg删除 [rootexample ~]# rm -rf /boot/grub2/grub.cfg 2.重启服务器 3&#xff0c;发现进入系统失败 输入以下命令 ls: 列出当前设备上的文件和目录。 grub> ls (hd0) (hd0,msdos3) (hd0,msd0s2) (hd0,msdos1) #一般第一个为/boot分区se…

2月8日作业

1、现有文件test.c\test1.c\main.c,编写Makkefile 代码&#xff1a; CCgcc EXEa.out OBJS$(patsubst %.c,%.o,$(wildcard *.c)) CFLAGS-c -oall:$(EXE)$(EXE):$(OBJS)$(CC) $^ -o $%.o:%.c$(CC) $(CFLAGS) $ $^.PHONY:cleanclean:rm $(OBJS) $(EXE)运行结果&#xff1a; 2、…

wyh的迷宫

涉及知识点&#xff1a;求迷宫能否到达终点的&#xff0c;而不是求路径数的&#xff0c;用bfs时可以不用重置状态数组&#xff08;回溯&#xff09;。 题目描述 给你一个n*m的迷宫&#xff0c;这个迷宫中有以下几个标识&#xff1a; s代表起点 t代表终点 x代表障碍物 .代…

【C#】创建Json文件并根据dll路径获取

创建Json文件 更改属性 【代码】根据dll路径获取 Assembly assembly Assembly.GetExecutingAssembly(); string assemblyPath assembly.Location; string relativeDllPath System.IO.Path.Combine(System.IO.Path.GetDirectoryName(assemblyPath), "Json\\test.json&q…

Kubernetes基础(十五)-k8s网络通信

1 k8s网络类型 2 Pod网络 2.1 同一pod内不同容器通信 Pod是Kubernetes中最小的可部署单元&#xff0c;它是一个或多个紧密关联的容器的组合&#xff0c;这些容器共享同一个网络命名空间和存储卷&#xff0c;因此Pod中的所有容器都共享相同的网络命名空间和IP地址——PodIP&a…

华为第二批难题五:AI技术提升六面体网格生成自动化问题

有CAE开发商问及OCCT几何内核的网格方面的技术问题。其实&#xff0c;OCCT几何内核的现有网格生成能力比较弱。 HybridOctree_Hex的源代码&#xff0c;还没有仔细去学习。 “HybridOctree_Hex”的开发者说&#xff1a;六面体网格主要是用在数值模拟领域的&#xff0c;比如汽车…

[WUSTCTF2020]朴实无华(特详解)

一开始说header出问题了 就先dirsaerch扫一遍 发现robot.txt 访问一下 去看看&#xff0c;好好好&#xff0c;肯定不是得 他一开始说header有问题&#xff0c;不妨抓包看看&#xff0c;果然有东西 访问看看&#xff0c;乱码修复一下&#xff0c;在之前的博客到过 <img src…

一文带你读懂Python线程

Python线程 进程有很多优点&#xff0c;它提供了多道编程&#xff0c;可以提高计算机CPU的利用率。既然进程这么优秀&#xff0c;为什么还要线程呢&#xff1f;其实&#xff0c;仔细观察就会发现进程还是有很多缺陷的。 主要体现在一下几个方面&#xff1a; 进程只能在一个时…