优雅地解决外部依赖的UT问题 Testcontainer

发布于:2024-10-24 编辑:匿名 来源:网络

在我们日常的微服务开发中,难免会用到很多第三方的依赖服务。最典型的就是MySQL。

除此之外还有ZK、Redis、Mongo、MQ等。 、Consul、ES等。

众多中间件的使用也给测试过程带来了一定的复杂度。如果我想让我的产品的UT覆盖率达到>90%,那么依赖组件的UT是一件非常麻烦的事情。

大多数情况下,我们会使用skip方法将所有对中间件的依赖测试暴露给集成测试流程,希望通过产品功能的测试覆盖中间件使用的测试。当然,当不需要UT覆盖时,面向依赖的UT也应该是有价值的,并且是研发过程中不可或缺的一部分。

不针对中间件测试会给我们的代码留下足够的隐患。为什么我们需要依赖UT?是不是可以使用Mock(绕过)?在没有合适的中间价UT方式的情况下,我们大多数人会在UT过程中使用Mock来绕过DAO层对gorm的使用。

以MySQL为例,我们做一个简单的demo。完整的代码可以通过github获取。

代码语言: go copy var DB *gorm.DBtype Product struct {Code stringPrice int}type Repository struct {}func NewRepository() *Repository {return &Repository{}}func OpenDB(dbUrl string) (*gorm.DB, error) { return gorm.Open(mysql.Open(dbUrl), &gorm.Config{})}func (r *Repository) Select() (Product, error) {var Product Producterr := DB.First(&product, "code = ?" , "D42").Error // 查找code字段值为D42的记录 return Product, err}func (r *Repository) Create(product Product) error {return DB.Create(&product).Error}DAO层使用gorm定义公共变量 DB *gorm.DB 用于全局 MySQL 连接。 OpenDB(dbUrl string)用于根据地址获取连接。

Create和Select分别用于创建数据和查询一定条件下的数据。代码语言: go copy func init() {db, err := dao.OpenDB("")if err != nil {panic(err)}dao.DB = db}func QueryData() (*dao.Product, error ) {r := dao.NewRepository()product, err := r.Select()if err != nil {return nil, err}err = DoSomethingUseProduct(product)return &product, err}func DoSomethingUseProduct(product dao.Product) error {//todofmt.Println(product)return nil} 我们使用 init 方法创建一个数据库连接,并在运行程序之前将其初始化为公共连接。

QueryData使用Select来查询数据,使用Dosomething来完成一些业务逻辑。现在我们开始为 QueryData 编写一个 UT,大概应该是这样的。

这里我们使用Byte开源github.com/bytedance/mockey包。代码语言: go copy func TestQueryData(t *testing.T) {mockey.PatchConvey("22", t, func() {mockey.Mock((*dao.Repository).Select).Return(dao.Product{Price : 1,}, nil).Build()defer mockey.UnPatchAll()mockey.Mock(DoSomethingUseProduct).Return(nil).Build()product, err := QueryData()assert.Nil(t, err)assert. Equal(t, 1, Product.Price)dao.DB = nil})} 无法连接到本地连接数据库。

我们会优先进行mock来绕过gorm层的真正执行,让UT继续进行。 *dao.Repository).Select方法执行不能被ut覆盖。

说到这里,有些老家伙会有几个疑问。—————————————————————————————————————————————————— ——Q1 如果在本地创建一个mysql,导入表结构不就解决问题了吗?答:一般来说,商业项目是由多人完成的。

如果A在代码中添加了需要本地部署环境的单元测试代码,那么在B、C、D等中,如果大家都需要执行ut,就需要部署环境,甚至初始化相同的数据。如果项目需要在CI环境中执行,还需要部署环境。

代码可读性差,复用性低。如果项目还依赖其他中间件,各部署一套成本就有点高了。

Q2 DAO层只是一些简单的SQL增删改查逻辑,不需要通过ut进行测试。 A:引入中间件是因为业务逻辑必须依赖它。

也就是说,由于你使用了MySQL这样的中间件,并且必须有很强的依赖关系,所以当出现执行错误时,就意味着业务逻辑出现了问题。如果是简单的增删改查功能,在产品功能验收时可能会被覆盖,但有些复杂的产品功能是基于复杂的数据组合来完成的。

举个简单的例子,一个列表页有10个字段,需要根据每个字段进行过滤和排序。实现该功能的代码可以是如下代码语言: go copy func Query(condition *QueryCondition) []*Resp { db := dao.GetDB().Select("*") if condition.Field1 != nil { db = db.where("Field1 = ?", condition.Field1) } if condition.Field2 != nil { db = db.where("Field2 = ?", condition.Field2) } ......(other if ) if condition.Field10 != nil { db = db.where("Field10 = ?", condition.Field10) } .......(其他分页排序逻辑)} 基于这个例子,因为 Query 方法是一个底层方法,上层可能会有f1、f2、f3等一系列调用,最终形成一个复杂的逻辑网络。

通过产品功能验收可能无法覆盖所有组合场景。假设其中一个条件在编写时存在字段或语法错误,并且在产品功能测试时未覆盖。

当它上线并被用户在使用中发现时,就已经太晚了。 (根据真实案例描述,产品上线后发现SQL语法错误,最终导致产品收入严重损失)————————————————— —————————————————————————————————————————这里我们回归正题。

mysql gorm层mock无非就是以下几种场景。插入mock return "err is nil" 更新mock return "err is nil" 删除mock return "err is nil" Select Mock return "err is nil and data is mock_data" 除了select mock data,其他的好像没什么意义,而它们实际上是毫无意义的。

因为,如上面的例子,执行SQL并不总是成功,Error也是存在的。比如常见的语法错误、字段拼写错误、数据格式、时间格式错误等。

那么这些Error只有在集成测试过程中才能发现。对于逻辑不复杂的功能点,部署测试链路并进行FT即可发现问题。

但在业务开发中,总有一些逻辑复杂的FT环节,属于黑盒测试。如何保证每一个if都能被测试?其次,即使在FT过程中发现问题,仍然需要人力返工修复,然后部署,再次测试,再次失败,再次修复……(即使云原生环境支持快速部署,还是让开发者心态崩溃)那么如何解决依赖测试呢?比如上面提到的MySQL,最简单的方式就是我们可以在本地部署一个MySQL,然后连接测试,但是有几个问题:用例无法复用,A写的用例B由于缺少环境;部署的CI/CD环境还需要安装MySQL,需要太多依赖;如果还依赖其他组件,比如ZK、Redis、ES等,每个组件都需要安装在本地开发环境中,成本高、成本高。

如果环境运行时间较长,多个依赖被占用,实时拉起需要很长时间;而今天介绍的神器Testcontainer就完美解决了这一系列问题。Testcontainer工具介绍 Testcontainers是一个开源的三方依赖库,用于支持单元测试。

它提供了一个简单且轻量级的 API,用于使用打包在 Docker 容器中的真实服务来启动本地开发和测试依赖中间件。 。

通过使用测试容器,您可以编写依赖于与生产环境相同的服务的测试,而无需使用模拟对象或内存中服务。简单来说,它只是一个依赖库lib,而不是一个服务。

其次,通过Docker容器快速创建自己需要的依赖服务器并提供使用。它可以支持所有可容器化的外部依赖,并且支持多种常见的编程语言和几乎所有常用的中间件。

拥有完善的容器创建和自动回收机制,使用过程中无需关注容器回收。想要了解更多的同学可以访问官方网站。

testcontainers官网 使用TestContainer按需隔离基础设施配置的优点: 不需要预先配置集成的测试基础设施。测试容器将在运行测试之前提供所需的服务。

即使多个构建管道并行运行,也不会有测试数据污染的可能性,因为每个管道都运行一组独立的服务。在本地和 CI 环境中获得一致的体验:您可以直接从 IDE 运行集成测试,就像运行单元测试一样。

无需推送更改并等待 CI 管道完成。使用等待策略的稳健测试设置:在测试中使用 Docker 容器之前,需要启动并完全初始化它们。

Testcontainers 库提供了几种开箱即用的等待策略实现,以确保容器(以及其中的应用程序)完全初始化。 Testcontainers 模块已经实现了给定技术的相关等待策略,并且您始终可以根据需要实现自己的策略或创建复合策略。

高级网络功能:测试容器库将容器的端口映射到主机上可用的随机端口,以便您的测试可靠地连接到这些服务。您甚至可以创建一个 (Docker) 网络并将多个容器连接在一起,以便它们通过静态 Docker 网络别名相互通信。

自动清理:测试执行完成后,Testcontainers 库会自动删除使用 Ryuk sidecar 容器创建的任何资源(容器、卷、网络等)。当启动所需的容器时,Testcontainers 会将一组标签附加到创建的资源(容器、卷、网络等),Ryuk 通过匹配这些标签自动执行资源清理。

即使测试进程异常退出(例如发送SIGKILL),它也能可靠地工作。实际DEMO是基于上面的测试代码。

我们基于它创建并使用 TestContainer 进行单元测试。加载Testcontainer依赖库代码语言:javascript copy##demo go版本为go_1.19,对应版本号为v0.20##根据需要测试的对象选择modules包。

其他的可以去代码仓库Tag找到。 ## 获取 github.com/testcontainers/ 获取 github.com/testcontainers/testcontainers-go/modules/。

20.0##如果需要其他组件请获取 github.com/testcontainers/testcontainers-go/modules/ 为 UT 创建容器 创建 testhelper.go 文件,用于编写依赖的容器创建代码 代码语言:javascript copy func init( ) { if dao.DB != nil { return } err, mysqlTestUrl := CreateTestMySQLContainer(context.Background()) if err != nil { 恐慌(err) } dao.DB, err = dao.OpenDB(mysqlTestUrl ) if err != nil { 恐慌(err) }}func CreateTestMySQLContainer(ctx context.Context) (error, string) { 容器, err := mysql.RunContainer(ctx, testcontainers.WithImage("mysql:8.0"), mysql .WithDatabase( “test_db”),mysql.WithUsername(“root”),mysql.WithPassword("root@"), //也可以使用sql脚本初始化数据库 //mysql.WithScripts(filepath.Join("..", "testdata", "init-db.sql") ) if err != nil { return err, "" } //获取访问连接 str, err := container.ConnectionString(ctx) if err != nil { return err, "" } //打印连接,即可登录本地环境通过连接构建mysql日志 .Printf("can use thisconnecting string to login in db:%s", str) return nil, str}//如果需要其他依赖容器,可以类似创建/ /func CreateTestRedisContainer(ctx context.Context) error {}//func CreateTestZKContainer(ctx context.Context) error {}我们知道go的import加载机制是先执行import引入依赖中的init()方法,然后再执行init放在自己的包中,然后执行调用代码。这里我们使用init方法来创建初始的mysql docker容器并初始化全局DB连接。

当UT需要测试dao层时,导入路径即可。其他团队的开发人员以后不需要关注容器的创建。

使用TestContainer编写UT代码语言: go copy func TestQueryDataUseContainer(t *testing.T) {mockey.PatchConvey("23", t, func() {//初始化需要测试的表,并初始化err需要测试哪些表: = dao.DB.AutoMigrate(dao.Product{})assert.Nil(t, err)r := dao.NewRepository()//写入临时测试数据 err = r.Create(dao. Product{Code: "D42 ",Price: 1,})assert.Nil(t, err)//执行测试mockey.Mock(DoSomethingUseProduct).Return(nil).Build()product, err := QueryData() assert.Nil(t, err )assert.Equal(t, 1, Product.Price)})} 从运行结果可以看到,在ut的执行过程中确实进行了真正与mysql相关的操作,这样我们的代码不再需要部署到特殊环境。完成测试并具有一定的覆盖率。

例如Redis、MQ、Kakfa、ES等中间件依赖都可以用同样的方式进行测试。其他问题 Q:引入TestContainer创建测试容器会不会占用资源或者导致我们的UT耗时较长?经测试,MAC本地研发环境下MySQL容器启动时间<20s。

在纯粹的CI/CD环境中,相信会有更好的性能,所以不需要担心资源占用。容器启动占用资源很少,比本地安装MySQL要好。

肯定少了很多,而且用完后还会回收。 Q:容器是否需要进行管理,比如使用后关闭、释放资源,避免资源泄漏?测试执行完成后,Testcontainers库将使用Ryuk sidecar容器自动删除任何创建的资源(容器、卷、网络等),即使测试进程异常退出(例如发送SIGKILL)并且可靠工作。

当TestContainer运行时,所有容器完成后会自动回收。但如果同时测试很多中间件,可以进行编排,避免容器同时拉起,造成一定的资源损失。

如果您有更好的见解或者疑问,请在评论区留言。

优雅地解决外部依赖的UT问题 Testcontainer

站长声明

版权声明:本文内容由互联网用户自发贡献,本站不拥有所有权,不承担相关法律责任。如果发现本站有涉嫌抄袭的内容,欢迎发送邮件 举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。

标签:

相关文章

  • 美媒:微软将以超500亿美元收购动视暴雪,成为公司历史上最大一笔收购

    美媒:微软将以超500亿美元收购动视暴雪,成为公司历史上最大一笔收购

    彭博1月18日消息,通报人士表示,微软即将达成收购动视暴雪的交易,这将成为微软历史上规模最大的收购。 据《华尔街日报》报道,微软收购动视暴雪的金额将超过1亿美元。 据报道,该交易最早可能在当地时间周二宣布。 一位内部人士表示,这笔交易将使动视暴雪的估值接近1亿美元

    06-17

  • 长安自动驾驶汽车路测结束,创纪录

    长安自动驾驶汽车路测结束,创纪录

    五年前,长安汽车开始布局智能汽车,后来成为其战略方向,累计投入智能汽车技术研发和应用将达50亿元。 产品开发。 4月12日到4月16日,他们几年来的研究成果之一——长安自动驾驶汽车决定大显身手。 经过五天近公里路试,从重庆北上至北京。 4月17日,长安汽车在北京举行了本

    06-17

  • 消失的海外大片

    消失的海外大片

    在旅游市场和杭州亚运会的热闹气氛中,今年的国庆节终于落下帷幕。 猫眼专业版数据显示,今年国庆档电影播放量已突破27亿,整体增长趋势较为良好。 国庆期间单日票房超过2亿元,观影总人数达万人次。 从电影类型来看,今年国庆电影包括剧情、喜剧、爱情、动画等多种类型,其

    06-17

  • 广西动员“专精特新”企业冲刺北交所设立10亿元专业孵化基金

    广西动员“专精特新”企业冲刺北交所设立10亿元专业孵化基金

    头条新闻是9月12日,“新三板”改革发展座谈会广西“专精特新”企业上市推进会在广西南宁举行。 广西投资集团党委副委员书记李斌在致辞中表示,广西投资集团通过旗下专业基金和股权投资平台设立了广西中小企业孵化基金。 基金总规模10亿元。 会议宣布实施国家创新驱动发展战略

    06-18

  • 投资界24h-27.6万Model Y来了;传Keep Himalaya取消赴美IPO;字节跳动或将重启飞聊

    投资界24h-27.6万Model Y来了;传Keep Himalaya取消赴美IPO;字节跳动或将重启飞聊

    时间:2020年7月9日星期五重要消息第一家社区团购独立公司倒下角落:又一家明星独角兽以65亿估值倒塌。 昨晚(7月7日),社区团购巨头-同程人寿母公司苏州橙子科技有限公司发布公告称,因经营不善,公司决定申请破产,现拟申请破产保护。 破产后,将依法推进债务处置。 。 就

    06-17

  • “最大人工智能公司”阿里巴巴AI全景首次全面曝光

    “最大人工智能公司”阿里巴巴AI全景首次全面曝光

    雷锋网云栖大会现场报道。 飞天智能主论坛第二天,阿里巴巴集中展示了其整个AI格局。 阿里巴巴的科学家和工程师轮流上台。 这是一个信息量巨大的主论坛,这也是一篇信息量巨大的文章。 温馨提示,如需浓缩版,请滚动至本文末尾阅读摘要。 阿里云飞天智能“最强团队”由十人组

    06-18

  • 59岁的医学博士今天又赢得了IPO

    59岁的医学博士今天又赢得了IPO

    又一个医学博士站上台敲响IPO的钟声。 据投资界4月29日消息,眼科药企兆科眼科有限公司(以下简称“兆科眼科”)在香港联交所成功上市。 本次IPO中,兆科眼科的发行价为每股16.8港元。 截至发稿,总市值近80亿港元。 在分拆上市前,兆科眼科是李氏大药厂(0.HK)的联营公司。

    06-17

  • 为什么要大众创业、万众创新? -谈大众创业、万众创新

    为什么要大众创业、万众创新? -谈大众创业、万众创新

    引言:近两年,每当经济出现波动、质疑声出现时,不可或缺的诟病对象就是“大众创业、万众创新”。 作为本届政府最重要的政策之一,批评者的视角普遍与上届政府的“四万亿”计划类似。 比如大众创业、万众创新浪费社会资源、通过补贴制造经济泡沫、创业误导大学生等等。 从大

    06-18

  • 盛大游戏上市创造中国企业三项历史记录

    盛大游戏上市创造中国企业三项历史记录

    据9月26日消息,据媒体报道,今天刚刚完成分拆上市的盛大游戏,创造了三项历史记录:全球最大的中国网络游戏公司上市;中国互联网公司在美国上市规模最大;中国公司在美国的单一上市规模最大。   据悉,盛大及盛大游戏以每股12.50美元的价格发行10,000股美国存托股票(ADS)

    06-17

  • 英特尔或与Apollo达成协议,建设爱尔兰晶圆厂

    英特尔或与Apollo达成协议,建设爱尔兰晶圆厂

    据外媒报道,有消息称,英特尔正在与私募股权公司Apollo(Apollo Global Management)进行深入谈判,这将为英特尔提供更多1亿元以上。 提供美元资金帮助英特尔在爱尔兰建造一座新晶圆厂。 该交易可能会在未来几周内完成。 报道指出,在Apollo牵头之前,包括KKR和Stonepeak在内

    06-06

  • 天猫双十一:45分钟超300个品牌销量过亿

    天猫双十一:45分钟超300个品牌销量过亿

    投资界(ID:pedaily)据11月11日消息,记者从天猫获悉,今年天猫双11,多家中小型企业品牌实现了跨越式增长。 截至11月11日0时45分,去年成交额过百万的中小品牌,今年已突破千万;去年双11成交额过千万的品牌还有40个,今年双11销售额突破亿元大关。 此外,11月1日至11日零

    06-18

  • 无人机公司傲势科技获盘林资本A轮融资

    无人机公司傲势科技获盘林资本A轮融资

    据投资界5月2日消息,近日,四川傲势科技股份有限公司(以下简称“傲势科技”)获得盘林资本领投A 系列融资。   傲势科技于年底成立。 是目前国内顶尖的行业级无人机研发生产企业。 该产品为小型垂直起降电动无人机系统,用于城市治安巡逻、反恐应急、环保监测等领域,飞行

    06-18