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

devtools/2025/3/26 1:48:46/

项目地址: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)

goneConfigureMock_118">对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代码进行单元测试?》

http://www.ppmy.cn/devtools/170689.html

相关文章

C++:背包问题习题

1. 货币系统 1371. 货币系统 - AcWing题库 给定 V 种货币(单位:元),每种货币使用的次数不限。 不同种类的货币,面值可能是相同的。 现在,要你用这 V 种货币凑出 N 元钱,请问共有多少种不同的…

滑动窗口思想的介绍与单调队列代码实现

活动发起人小虚竹 目录 前言 滑动窗口的基本思想 举例说明 239. 滑动窗口最大值 - 力扣(LeetCode) 暴力做法 滑动窗口优化 算法分析 使用例题数据进行模拟 输入 初始状态 步骤解析 前言 本篇博客我们介绍滑动窗口,这是一种特殊的双指针技巧思…

SQL Server常见问题解析

SQL Server常见问题的分类解析及详细解决方案: 一、连接与访问问题 远程连接失败 关键检查项: (1) SQL Server配置管理器中启用TCP/IP协议及Named Pipes (2) SQL Server Browser服务运行状态 (3) 防火墙规则设置(默认端口1433/TCP&#xf…

[入门]NUC13配置Ubuntu20.04详细步骤

文章目录 1. 安装Ubuntu20.041.1 制作系统启动盘1.1.1 下载镜像文件1.1.2 配置启动盘 1.2 安装内存条、硬盘1.3 安装系统 2. 网卡驱动配置2.1 关闭安全启动2.2 安装intel官方网卡驱动backport2.2.1 第四步可能会出现问题 2.3 ubuntu官方的驱动2.4 重启 3. 软件安装3.1 录屏软件…

Ai客服机器人系统源码

我将基于常见的自然语言处理库,用 Python 编写一个简单的 AI 客服机器人功能代码示例,它能处理常见问题并根据用户输入提供相应回复。 import nltk​ from nltk.chat.util import Chat, reflections​ ​ # 下载必要的NLTK数据​ nltk.download(pun…

uni-app集成保利威直播、点播SDK经验FQ(二)|小程序直播/APP直播开发适用

通过uniapp集成保利威直播、点播SDK来开发小程序/APP的视频直播能力,在实际开发中可能会遇到的疑问和解决方案,下篇。更多疑问请咨询19924784795。 1.ios不能后台挂起uniapp插件 ios端使用后台音频播放和画中画功能,没有在 manifest.json 进…

《信息系统安全》(第一次上机实验报告)

实验一 :网络协议分析工具Wireshark 一 实验目的 学习使用网络协议分析工具Wireshark的方法,并用它来分析一些协议。 二实验原理 TCP/IP协议族中网络层、传输层、应用层相关重要协议原理。网络协议分析工具Wireshark的工作原理和基本使用规则。 三 实…

深入理解计算机网络:OSI 与 TCP/IP 各层结构与功能

目录 1. 引言 2. OSI 模型 2.1 OSI 各层的详细功能 2.1.1 物理层 2.1.2 数据链路层 2.1.3 网络层 2.1.4 传输层 2.1.5 会话层 2.1.6 表示层 2.1.7 应用层 3. TCP/IP 模型 3.1 TCP/IP 各层的详细功能 3.1.1 网络接口层 3.1.2 网络层 3.1.3 传输层 3.1.4 应用层 …