当前位置: 首页 > article >正文

如何基于Gone编写一个Goner对接Apollo配置中心(下)—— 对组件进行单元测试

项目地址:https://github.com/gone-io/gone

原文地址:https://github.com/gone-io/goner/blob/main/docs/test_goner.md

本文介绍的例子,代码在:https://github.com/gone-io/goner/blob/main/apollo

文章目录

    • 引言
    • 编写“可测试”的代码
    • 对外部模块进行Mock
      • 对`gone.Configure`的Mock
      • 对`startWithConfig`的Mock
    • 编写测试代码
      • 测试初始化逻辑
      • 测试配置获取功能
      • 测试配置变更监听功能
    • 总结

引言

在上一篇文章《如何基于Gone编写一个Goner对接Apollo配置中心(上)—— 实现统一管理配置和监控配置变化》中,我们详细介绍了如何在Gone框架中实现一个Apollo配置中心组件。然而,仅仅实现功能是不够的,为了确保组件的可靠性和稳定性,我们必须为其编写充分的单元测试。本文以Apollo组件为例,深入探讨如何在Gone框架中构建高质量的单元测试,帮助开发者打造更健壮的组件。

编写“可测试”的代码

正如我在另一篇文章《如何对Golang代码进行单元测试?》中提到的,编写单元测试的前提是编写“可测试”的代码,并采用设计可测试代码的实践方法。以以下代码为例,我们需要思考:

  • 需要测试哪些部分?
  • 如何对这些部分进行测试?
func (s *apolloClient) Init() {s.localConfigure = viper.New(s.testFlag)m := map[string]*tuple{"apollo.appId":                     {v: &s.appId, defaultVal: ""},"apollo.cluster":                   {v: &s.cluster, defaultVal: "default"},"apollo.ip":                        {v: &s.ip, defaultVal: ""},"apollo.namespace":                 {v: &s.namespace, defaultVal: "application"},"apollo.secret":                    {v: &s.secret, defaultVal: ""},"apollo.isBackupConfig":            {v: &s.isBackupConfig, defaultVal: "true"},"apollo.watch":                     {v: &s.watch, defaultVal: "false"},"apollo.useLocalConfIfKeyNotExist": {v: &s.useLocalConfIfKeyNotExist, defaultVal: "true"},}for k, t := range m {err := s.localConfigure.Get(k, t.v, t.defaultVal)if err != nil {panic(err)}}c := &config.AppConfig{AppID:          s.appId,Cluster:        s.cluster,IP:             s.ip,NamespaceName:  s.namespace,IsBackupConfig: s.isBackupConfig,Secret:         s.secret,}client, err := agollo.StartWithConfig(func() (*config.AppConfig, error) {return c, nil})if err != nil {panic(err)}s.apolloClient = clientif s.watch {client.AddChangeListener(s.changeListener)}
}

针对上述代码的测试较为困难,主要原因在于它依赖了两个外部系统:viperagollo。其中,对于viper我们可以通过本地配置文件或环境变量来解决,而对于agollo则需要搭建一套Apollo服务,这在自动化测试环境中成本较高。
因此,我们应关注的是apolloClient的初始化逻辑,而不必测试viper的配置读取或agollo的启动。为此,可以将对外部模块的依赖进行外部化,改写后的代码如下:

func (s *apolloClient) init(localConfigure gone.Configure, startWithConfig func(loadAppConfig func() (*config.AppConfig, error)) (agollo.Client, error)) {type tuple struct {v          anydefaultVal string}m := map[string]*tuple{"apollo.appId":                     {v: &s.appId, defaultVal: ""},"apollo.cluster":                   {v: &s.cluster, defaultVal: "default"},"apollo.ip":                        {v: &s.ip, defaultVal: ""},"apollo.namespace":                 {v: &s.namespace, defaultVal: "application"},"apollo.secret":                    {v: &s.secret, defaultVal: ""},"apollo.isBackupConfig":            {v: &s.isBackupConfig, defaultVal: "true"},"apollo.watch":                     {v: &s.watch, defaultVal: "false"},"apollo.useLocalConfIfKeyNotExist": {v: &s.useLocalConfIfKeyNotExist, defaultVal: "true"},}for k, t := range m {err := localConfigure.Get(k, t.v, t.defaultVal)if err != nil {panic(err)}}c := &config.AppConfig{AppID:          s.appId,Cluster:        s.cluster,IP:             s.ip,NamespaceName:  s.namespace,IsBackupConfig: s.isBackupConfig,Secret:         s.secret,}client, err := startWithConfig(func() (*config.AppConfig, error) {return c, nil})if err != nil {panic(err)}s.apolloClient = clientif s.watch {client.AddChangeListener(s.changeListener)}
}func (s *apolloClient) Init() {s.localConfigure = viper.New(s.testFlag)s.init(s.localConfigure, agollo.StartWithConfig)
}

通过这种改造,我们可以在测试时只关注init()函数的逻辑,而不必依赖实际的外部模块,从而大大降低了测试成本。

对外部模块进行Mock

针对改造后的init()函数,其依赖主要集中在两个方面:

  • localConfigure(类型为gone.Configure
  • startWithConfig函数(签名为func(loadAppConfig func() (*config.AppConfig, error)) (agollo.Client, error)

gone.Configure的Mock

我们可以利用mockgen工具直接生成接口的模拟实现,命令如下:

go install go.uber.org/mock/mockgen@latest
mockgen -package=apollo github.com/gone-io/gone/v2 Configure > gone_mock_test.go

startWithConfig的Mock

首先,利用mockgen生成agollo.Client接口的模拟实现:

mockgen -package=apollo github.com/apolloconfig/agollo/v4 Client > agollo_mock_test.go

然后,为测试startWithConfig构建一个模拟函数:

mockClient := NewMockClient(ctrl)
mockedStartWithConfig = func(loadAppConfig func() (*config.AppConfig, error)) (agollo.Client, error) {return mockClient, nil
}

编写测试代码

测试初始化逻辑

该测试用例主要验证以下几点:

  1. 配置项是否正确读取
  2. 默认值是否生效
  3. Apollo客户端是否被正确创建
func TestApolloClient_Init(t *testing.T) {ctrl := gomock.NewController(t)defer ctrl.Finish()// 创建模拟对象localConfigure := NewMockConfigure(ctrl)// 设置模拟对象的行为localConfigure.EXPECT().Get("apollo.appId", gomock.Any(), "").Return(nil).Do(func(key string, v any, defaultVal string) {*(v.(*string)) = "testApp"},)// ... 对其他配置项进行相应的Mock设置 ...mockClient := NewMockClient(ctrl)// 创建apolloClient实例client := &apolloClient{changeListener: &changeListener{},}client.localConfigure = localConfigure// 执行初始化client.init(localConfigure, func(loadAppConfig func() (*config.AppConfig, error)) (agollo.Client, error) {return mockClient, nil})// 验证配置是否正确读取assert.Equal(t, "testApp", client.appId)assert.Equal(t, "default", client.cluster)// ... 对其他配置项进行验证 ...
}

测试配置获取功能

此测试用例涵盖了以下场景:

  1. 成功从Apollo获取配置
  2. 当Apollo获取失败时,能够回退到本地配置
  3. 禁用本地配置时的行为
func TestApolloClient_Get(t *testing.T) {ctrl := gomock.NewController(t)defer ctrl.Finish()// 创建模拟对象localConfigure := NewMockConfigure(ctrl)mockClient := NewMockClient(ctrl)mockCache := NewMockCacheInterface(ctrl)// 设置模拟对象的行为mockClient.EXPECT().GetConfigCache("application").Return(mockCache).AnyTimes()mockCache.EXPECT().Get("test.key").Return("test-value", nil).AnyTimes()// 创建apolloClient实例client := &apolloClient{localConfigure:            localConfigure,apolloClient:              mockClient,namespace:                 "application",changeListener:            &changeListener{},watch:                     false,useLocalConfIfKeyNotExist: true,}// 测试从Apollo获取配置var value stringerr := client.Get("test.key", &value, "default-value")assert.Nil(t, err)assert.Equal(t, "test-value", value)// 测试在Apollo获取失败时使用本地配置mockCache.EXPECT().Get("test.not-exist").Return(nil, errors.New("key not found")).AnyTimes()localConfigure.EXPECT().Get("test.not-exist", gomock.Any(), "default-value").Return(nil).Do(func(key string, v any, defaultVal string) {*(v.(*string)) = "local-value"},)var localValue stringerr = client.Get("test.not-exist", &localValue, "default-value")assert.Nil(t, err)assert.Equal(t, "local-value", localValue)
}

测试配置变更监听功能

此测试用例主要验证:

  1. 配置监听是否正确注册
  2. 当配置发生变化时,值是否能被正确更新
func TestApolloClient_Get_WithWatch(t *testing.T) {ctrl := gomock.NewController(t)defer ctrl.Finish()// 创建并设置必要的模拟对象// ...// 创建changeListener并初始化listener := &changeListener{}listener.Init()// 创建apolloClient实例,设置watch为trueclient := &apolloClient{// ...watch: true,}// 测试获取配置时,带有监听功能var value stringerr := client.Get("test.key", &value, "default-value")assert.Nil(t, err)assert.Equal(t, "test-value", value)// 验证监听器是否正确注册了该key_, exists := listener.keyMap["test.key"]assert.True(t, exists)// 模拟配置变更通知changes := make(map[string]*storage.ConfigChange)changes["test.key"] = &storage.ConfigChange{OldValue:   "test-value",NewValue:   "new-value",ChangeType: storage.MODIFIED,}changeEvent := &storage.ChangeEvent{Changes: changes,}// 触发配置变更通知listener.OnChange(changeEvent)// 验证配置值是否已被更新assert.Equal(t, "new-value", value)
}

总结

通过上述测试用例,我们实现了对Apollo组件核心功能的全面覆盖,主要体现在以下几点:

  1. 依赖注入与接口抽象
    将外部依赖(如viper和agollo)外部化,使代码具备更好的可测试性。

  2. Mock外部模块
    使用mockgen生成模拟对象,避免了在测试环境中对实际Apollo服务的依赖,大大降低了测试成本。

  3. 完善的测试场景设计
    覆盖了配置读取、获取和变更监听等关键功能,确保组件在各种场景下均能稳定运行。

  4. 提升代码可维护性
    通过单元测试为后续代码维护和重构提供了可靠保障,同时也为其他Gone组件的开发提供了可借鉴的测试方法。

这种测试方法不仅能够确保组件功能的正确性,还能显著提高代码质量和开发效率,是构建健壮系统的重要实践。


相关内容

  • 《如何基于Gone编写一个Goner对接Apollo配置中心(上)—— 实现统一管理配置和监控配置变化》
  • 《如何对Golang代码进行单元测试?》

相关文章:

如何基于Gone编写一个Goner对接Apollo配置中心(下)—— 对组件进行单元测试

项目地址:https://github.com/gone-io/gone 原文地址:https://github.com/gone-io/goner/blob/main/docs/test_goner.md 本文介绍的例子,代码在:https://github.com/gone-io/goner/blob/main/apollo 文章目录 引言编写“可测试”的…...

走进Java:String字符串的基本使用

❀❀❀ 大佬求个关注吧~祝您开心每一天 ❀❀❀ 目录 一、什么是String 二、如何定义一个String 1. 用双引号定义 2. 通过构造函数定义 三、String中的一些常用方法 1 字符串比较 1.1 字符串使用 1.2 字符串使用equals() 1.3 使用 equalsIgnoreCase() 1.4 cpmpareTo…...

python系列之元组(Tuple)

不为失败找理由,只为成功找方法。所有的不甘,因为还心存梦想,所以在你放弃之前,好好拼一把,只怕心老,不怕路长。 python系列之元组(Turple) 一、元组是什么?——给新手的…...

破解验证码新利器:基于百度OCR与captcha-killer-modified插件的免费调用教程

破解验证码新利器:基于百度OCR与captcha-killer-modified插件的免费调用教程 引言 免责声明: 本文提供的信息仅供参考,不承担因操作产生的任何损失。读者需自行判断内容适用性,并遵守法律法规。作者不鼓励非法行为,保…...

批量删除 PPT 中的所有图片、某张指定图片或者所有二维码图片

PPT 文档中的图片如何删除呢?相信很多小伙伴或碰到类似的需求。比如我们需要删除 PPT 文档中的某一张图片或者某张二维码图片,如果每一页都有这张图片,或者有很多 ppt 都有同一张要删除的图片,我们应该怎么快速的完成删除呢&#…...

大模型开发(六):LoRA项目——新媒体评论智能分类与信息抽取系统

LoRA项目——新媒体评论智能分类与信息抽取系统 0 前言1 项目介绍1.1 项目功能1.2 技术原理1.3 软硬件环境1.4 项目结构 2 数据介绍与处理2.1 数据集介绍2.2 数据处理2.3 数据导入器 3 模型训练3.1 配置文件3.2 工具函数3.3 模型训练3.4 模型评估 4 模型推理 0 前言 微调里面&…...

mysql-innodb存储引擎主键索引叶子结点数据结构(非单纯的双向链表)

我们应该清楚行记录是放在页中的。 compact行记录格式: 主要介绍几个比较重要的参数 heap_no: 页号 record_type: 0 表示普通类型(叶子结点),1表示B树的非叶子节点 ,2 表示最小记录&#xff…...

MySQL 进阶学习文档

一、存储引擎 1.1 核心架构 四层架构:连接层 → 服务层 → 引擎层 → 存储层插件式存储引擎:不同引擎独立管理数据存储,可动态选择 1.2 主流引擎对比 特性InnoDB(默认)MyISAMMemory事务支持✅ 支持❌ 不支持❌ 不支…...

物联网为什么用MQTT不用 HTTP 或 UDP?

先来两个代码对比,上传温度数据给服务器。 MQTT代码示例 // MQTT 客户端连接到 MQTT 服务器 mqttClient.connect("mqtt://broker.server.com:8883", clientId) // 订阅特定主题 mqttClient.subscribe("sensor/data", qos1) // …...

Vmware中的centos7连接上网

有很多刚刚开始配置了centos7,然后发现不能上网现在来解决这个问题。 测试能不能上网 先还原这个设置,如果没有动过的话就不用,连接模式是NAT模式 然后进去设置网络环境,记得是用超级用户设置 vi /etc/sysconfig/network-script…...

【AI知识】常见的优化器及其原理:梯度下降、动量梯度下降、AdaGrad、RMSProp、Adam、AdamW

常见的优化器 梯度下降(Gradient Descent, GD)局部最小值、全局最小值和鞍点凸函数和非凸函数动量梯度下降(Momentum)自适应学习率优化器AdaGrad(Adaptive Gradient Algorithm)​RMSProp(Root M…...

线性规划的标准形式

标准形式的定义 目标函数:最大化线性目标函数 其中,x 是决策变量向量,c 是目标系数向量。 约束条件:等式形式约束 A x b, 其中,A 是约束系数矩阵,b 是常数项向量。 变量非负约束: 。 因此…...

网络安全应急入门到实战

奇安信:95015网络安全应急响应分析报告(2022-2024年)官网可以下载 https://github.com/Bypass007/Emergency-Response-Notes 应急响应实战笔记 网络安全应急响应技术实战指南 .pdf 常见场景 第4章 勒索病毒网络安全应急响应 第5章 挖矿木…...

应用程序安全趋势:左移安全、人工智能和开源恶意软件

软件是大多数行业业务运营的核心,这意味着应用程序安全从未如此重要。 随着组织采用云原生架构、微服务和开源组件,攻击面不断扩大。结果是:攻击者渴望利用的易受攻击和恶意依赖项数量不断增加。 2025 年,安全团队将面临日益复杂…...

ospf动态路由

一、为什么使用动态路由 OSPF(open shortest path first开放最短路径优先)是内部网关协议(IGP)的一种,基于链路状态算法(LS)。 OSPF企业级路由协议(RFC2328 OSPFv2),核心重点协议 OSPF共三个版本,OSPFV1主要是实验室…...

【视频】OrinNX+Ubuntu20.04:移植OpenCV-4.11.0 with CUDA(含opencv_contrib )

1、源码下载 github下载地址如下,选择最新版本4.11 https://github.com/opencv/opencv/releases/tag/4.11.0 https://github.com/opencv/opencv_contrib/releases/tag/4.11.02、安装依赖库 1)对图片编码格式的支持 sudo apt install zlib1g-dev libjpeg8-dev libwebp-dev…...

基于单片机控制的电动汽车双闭环调速系统(论文+源码)

2.1系统方案 在本次设计中,其系统整个框图如下图3.1所示,其主要的核心控制模块由电源供电模块,晶振电路,驱动电路模块,霍尔传感器,按键模块,复位电路,LCD液晶显示及直流电机等组成。…...

【病毒分析】伪造微软官网+勒索加密+支付威胁,CTF中勒索病毒解密题目真实还原!

1.背景 该CTF挑战题目完整复现了黑客的攻击链路,攻击者通过伪造钓鱼页面引导受害者下载恶意软件。用户访问伪造的 Microsoft 365 官网后,在点击“Windows Installer (64-bit)”下载选项时,页面会自动跳转至伪造的 GitHub 项目链接&#xff0…...

PDF Reader Pro for Mac v4.9.0 PDF编辑/批注/OCR/转换工具 支持M、Intel芯片

PDF Reader Pro 是一款用户必备的集管理、编辑、转换、阅读功能于一体的专业的全能PDF阅读专家。快速、易用、强大,让您出色完成 PDF 工作。 应用介绍 PDF Reader Pro,一款功能齐全且强大的PDF阅读和编辑软件。支持PDF阅读、批注、PDF编辑、PDF格式转换…...

神经网络中层与层之间的关联

目录 1. 层与层之间的核心关联:数据流动与参数传递 1.1 数据流动(Forward Propagation) 1.2 参数传递(Backward Propagation) 2. 常见层与层之间的关联模式 2.1 典型全连接网络(如手写数字分类&#xf…...

PowerShell 美化 增强教程

PowerShell Windows Terminal 美化 & 增强教程 Windows Terminal PowerShell 默认外观和功能较为基础,但通过 Oh My Posh 及其他增强工具,你可以打造一个更美观、更高效的终端环境。本教程提供完整的安装、美化和优化步骤,包括常见问题…...

机械革命蛟龙16pro玩游戏闪屏

我查过原因,好像是AMD显卡对游戏用了可变刷新率就出bug了,可能是那个游戏不适合用可变刷新率技术。 解决办法: 1.桌面右键鼠标,出现如下标签,点击AMD Software:Adrenalin Edition 2.选择闪屏的游戏&#x…...

《AI赋能云原生区块链,引领供应链溯源革新》

在数字化浪潮席卷全球的当下,供应链管理领域正经历着深刻变革。云原生区块链凭借其去中心化、不可篡改等特性,为供应链溯源带来了前所未有的透明度与可靠性。而AI的融入,更如虎添翼,以强大的智能分析和决策能力,为云原…...

练习题:94

目录 Python题目 题目 题目分析 需求理解 关键知识点 实现思路分析 代码实现 代码解释 while 循环: 获取用户输入: 判断输入内容: 使用 break 语句: 处理非 "quit" 输入: 循环结束后的操作&am…...

实现图片多种处理需求的实用工具

在自媒体创作与日常办公时,图片处理常让人焦头烂额。今天就给大家介绍一款得力帮手——Fotosizer,它能帮你轻松批量处理图片,满足多样化需求。Fotosizer是一款功能强大的图片批量处理软件,无需安装,打开即用&#xff0…...

数据结构中的引用管理对象体系

数据结构中的引用管理对象体系 (注:似复刻变量即实例对象) 引用管理对象的,有引用就能管理到它所指向的对象,我们拿引用最终的目的就是管理那些我们需要管理的最终直接对象,引用也是对象,同时…...

Qwen2.5-VL 开源视觉大模型,模型体验、下载、推理、微调、部署实战

一、Qwen2.5-VL 简介 Qwen2.5-VL,Qwen 模型家族的旗舰视觉语言模型,比 Qwen2-VL 实现了巨大的飞跃。 欢迎访问 Qwen Chat (Qwen Chat)并选择 Qwen2.5-VL-72B-Instruct 进行体验。 1. 主要增强功能 1)直观地理解事物&…...

qyqt5项目打包成应用程序后,adb命令无效

问题:在Pycharm中执行以下代码能正常输出版本信息,但是使用pyinstaller 打包成pkg之后,运行软件一直都输出不了信息 version_info os.popen(f"adb version").read()解决方案: 配置adb 路径 adb_path os.getenv(ADB_…...

关于Docker是否被淘汰虚拟机实现连接虚拟专用网络Ubuntu 22.04 LTS部署Harbor仓库全流程

1.今天的第一个主题: 第一个主题是关于Docker是否真的被K8S弃用,还是可以继续兼容,因为我们知道在去年的时候,由于不可控的原因,docker的所有国内镜像源都被Ban了,再加上K8S自从V1.20之后,宣布…...

深入解析 `SQL_SMALL_RESULT`:MySQL 的“小优化”大作用

深入解析 SQL_SMALL_RESULT:MySQL 的“小优化”大作用 在 MySQL 的查询优化工具箱中,SQL_SMALL_RESULT 是一个容易被忽略但可能带来小幅性能提升的关键字。它适用于特定场景,尤其是涉及 GROUP BY 或 DISTINCT 计算的小数据集查询。本文将深入…...