美媒:微软将以超500亿美元收购动视暴雪,成为公司历史上最大一笔收购
06-17
在我们日常的微服务开发中,难免会用到很多第三方的依赖服务。最典型的就是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运行时,所有容器完成后会自动回收。但如果同时测试很多中间件,可以进行编排,避免容器同时拉起,造成一定的资源损失。
如果您有更好的见解或者疑问,请在评论区留言。
版权声明:本文内容由互联网用户自发贡献,本站不拥有所有权,不承担相关法律责任。如果发现本站有涉嫌抄袭的内容,欢迎发送邮件 举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。
标签:
相关文章
06-17
06-17
06-18
06-17
06-17
06-06
06-18
06-18
最新文章
【玩转GPU】ControlNet初学者生存指南
【实战】获取小程序中用户的城市信息(附源码)
包雪雪简单介绍Vue.js:开学
Go进阶:使用Gin框架简单实现服务端渲染
线程池介绍及实际案例分享
JMeter 注释 18 - JMeter 常用配置组件介绍
基于Sentry的大数据权限解决方案
【云+社区年度征文集】GPE监控介绍及使用