| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 开发测试 -> 一文说尽Golang单元测试实战的那些事儿 -> 正文阅读 |
|
[开发测试]一文说尽Golang单元测试实战的那些事儿 |
导语?|?单元测试,通常是单独测试一个方法、类或函数,让开发者确信自己的代码在按预期运行,为确保代码可以测试且测试易于维护。腾讯后台开发工程师张力结合了公司级漏洞扫描系统洞犀在DevOps上探索的经验,以Golang为例,列举了编写单元测试需要的工具和方法,然后针对写单测遇到的各种依赖问题,详细介绍了通过Mock的方式解决各种常用依赖,方便读者在写go语言UT的时候,遇到依赖问题,能够快速找到解决方案。最后再和大家探讨一下关于单元测试上的一些思考。 一、前言 单元测试,通常是单独测试一个方法、类或函数,让开发者确信自己的代码在按预期运行,为确保代码可以测试且测试易于维护。另一方面,DevOps里提倡自动化测试,并且主张越早发现代价越小。关于单元测试的更多思考,可以看看本文最后一节。本文结合了公司级漏洞扫描系统洞犀在DevOps上探索的经验,以Golang为例,列举了编写单元测试需要的工具和方法,然后针对写单测遇到的各种依赖问题,提出相应的解决办法,并展示了自动化单元测试的结果。最后再和大家探讨一下关于单元测试上的一些思考。 二、测试工具与方法 1.测试框架相信大家都熟悉go内置了go test测试框架来执行和管理测试用例。通过文件名_test.go结尾来表示测试文件,通过函数以Test开头并只有一个参数*testing.T来表示一个测试函数。例如:
而其中测试框架testing的类型*T提供了一系列方法,例如主要会用到的下面三个方法:
除此之外,还有其他用的比较多的测试包。例如断言包"github.com/stretchr/testify/assert",比如如果想判断返回的错误是否是空,如果用原生方法会是:
但用assert包只需要一行代码就可以实现上述功能,而且可以输出具体错误代码行:assert.Nil(t, err)。另外还有封装了testing的测试框架https://github.com/smartystreets/goconvey,里面包含了子测试断言等功能。 2.表格驱动测试 表格驱动测试通过定义一组不同的输入,可以让代码得到充分的测试,同时也能有效地测试负路径。?例如下面函数会判断参数类型,如果是int就乘以二,如果是string就先转成int然后乘以二,如果是其他类型就返回错误:
可以看到该函数有多个分支,如果要覆盖到不同分支,就需要不同类型输入,那么这就很适合表格驱动测试:
上面还用到了go test的子测试功能t.Run(name string, subTest func(t *T))。如果想在一个测试函数里面执行多个测试用例,例如要同时测试一个函数的返回成功和失败等各种情况,那么可以使用子测试来区分不同情况。 另外,上面表格测试代码框架是用Goland自动生成的,自己只需要填写tests数组就行了。点击函数名然后右键,选择generate,然后选择test for function就会自动生成测试函数了。不过上面生成的函数没有校验返回的错误内容,如有需要可以自己稍微修改一下。 三、解决常见的依赖等问题 解决常见的依赖等问题目前有两种思路:
下面几小节详细介绍了上述两种办法在不通场景下的应用,其中替换函数或方法、依赖接口类型和mysql数据库依赖对应了第一种思路;访问访问http接口、mysql数据库依赖和redis依赖对应了上面第二条思路。四、访问?http?接口 代码里经常会遇到要访问http接口的情况,这时如果在测试代码里不做处理直接访问,可能遇到环境不同访问不通等问题。为此go标准库内置了专门用于测试http服务的包net/http/httptest,不过我们这里并不用它来测试http服务,而是用来模拟要请求的http服务。基本流程是先创建一个路由器,然后注册一个响应函数用来模拟要请求的服务:
接着启动这个服务,httptest会真的在localhost启动它,然后这个URL就是要访问的服务地址了。
五、替换函数或方法 大家用的最多的应该就是monkey补丁库了,可以用它来替换各种函数和方法,使用起来非常方便,这类库原理大致相同,通过运行时用unsafe包替换函数地址来实现。比如https://github.com/agiledragon/gomonkey,不过这次我们用公司内部同源测试团队封装的monkey库来演示ngmock。首先是替换函数,新建一个函数mock对象,参数有*testing.T和要mock的函数。比如被测函数需要调用db.New新建一个DB,那么下面就mock了db.New函数。
然后在执行被测函数之前,设置mock函数接收什么参数,并且要返回什么,比如下面指定接收一个任意参数并且让db.New返回指定错误。该设置默认只会生效一次,如果要生效多次或者一直生效可以配置次数。
接下来就是执行被测函数函数来验证是否生效了,这里用到了上面提到的另一个测试框架convey,convey.Convey同*T.Run(),convey.So是 assert。
可以看到,mock依赖函数之后执行被测函数,会返回我们设置的错误fake error,在调用完成获得返回错误之后可以判断一下是否是我们设置的错误。还可以mock结构体方法,使用方式和上面类似,第二个参数传结构体或者指针,第三个是mock模式:
mock模式主要有两种:
如果在MacOS上执行测试遇到了permission denied的错误,这是 MacOS保护机制导致的,具体解决办法见https://github.com/eisenx p/macos-golink-wrapper 。 六、依赖接口类型 如果依赖的数据是接口类型,那么可以很方便的通过依赖注入的方式传入测试用的接口实现来替换原始依赖。go 官方出品的gomock 可以根据接口定义自动生成相应实现的mock桩代码:https://github.com/golang/ mock。gomock库会有个二进制文件mockgen用来生成代码,?比如文件里有一些接口定义:
? 可以执行mockgen来生成上述接口,具体命令如上,-destination指定生成文件名,-package是生成文件包名,-self_package指定生成的包路径,-source就是源接口文件路径名。如果最后不指定接口名的话,会生成所有接口或者可以指定要生成的接口,多个用逗号连接。?当然也可以读取标准库的接口:mockgen database/sql/driver Conn,Driver桩代码生成好了之后,就可以调用代码里类似 NewMockXXXX(ctrl)方法来创建mock对象,如下所示,这样创建的encoderMock实现了上面的Encoder接口,接下来就用这个encoderMock来初始化被测函数依赖的接口即可。
在调用被测函数之前,需要先打桩:我们希望如果encoderMock在执行Encode方法时传入会两个指定参数,那么就执行指定的函数并返回:
接下来执行被测函数,当实际调用到Encode方法时,就会执行我们设置的函数。看起来和上面一节的替换函数和方法类似是吧?这种希望当调用函数Encode()并且参数一致,那么就执行指定逻辑的方式,就是打桩(stub)。打桩过程还可以配置执行次数和执行顺序等,如果不知道打桩函数具体会被传入什么参数可以用gomock.Any()来代替。通过打桩可以控制依赖接口的行为,解决测试时接口依赖的问题。 七、mysql?数据库依赖 数据库依赖也是经常要遇到的一个问题,如何解决测试过程中的依赖呢?我这里总结了两种办法:?首先是sqlmock:https://github.com/DATA-DOG/go-sqlmock。看到mock字眼大家大概也知道它是怎么使用的了,也是通过对执行sql语句打桩来完成测试。首先初始化mock对象,返回第一个是*sql.DB,用来传给被测代码依赖的db,第二个就是mock对象,用来设置打桩代码。控制sqlDB的行为。
具体使用项目文档里有,我这里简单说一下:比如下面一个函数执行一些sql语句,先调用Begin创建事务,然后分别Query和Exec执行sql,最后如果返回错误则Rollback否则Commit。
那么针对上面函数,编写测试用例如下。其中打桩代码按照上面顺序,希望先执行Begin;然后执行Query,并且希望sql语句满足正则select .* from test并返回两行结果;然后执行Exec,希望 sql 满足正则update test并返回错误;最后执行Rollback。接下来执行被测函数,如果被测函数按照打桩代码的顺序执行相应sql的话就会返回指定内容,否则就会报错。
有时候我们的代码不会直接使用*sql.DB,而是用到一些第三方 ORM 框架,那么需要想办法让这些框架使用我们的 mock db,比如对于 gorm 框架,可以这么配置:
谈到gorm框架,那么问题来了,如果我不直接操作*sql.DB而是用的框架,但我不知道最后生成的sql是什么那该怎么办?或者说被测函数有一堆sql语句,一个一个打桩起来实在是太麻烦。那么对于这种情况如果能有一个本地数据库环境就好了,省去了打桩的麻烦,但是如果是mysql这种DB的话,本地建一个最快也是用容器跑才行。那么有没有更轻量化的办法呢? 可以本地临时创建一个sqlite数据库来代替当前依赖的数据库比如mysql等,sqlite是可以在本地直接跑的轻量级数据库,常见sql语句增删改查什么的和mysql区别不大。不过需要注意的是目前所有的go sqlite驱动都是基于CGO的,因为sqlite使用C写的。所以引用这些驱动会导致测试前程序编译速度变慢和跨平台支持问题,不过目前测试在MacOS和linux上是没有问题的。 如下所示首先创建一个临时的sqlite gorm框架DB,其中连接地址置空,这样在关闭db之后数据库也会自动删除。之后就可以正常使用了。它底层使用的是这个驱动github.com/mattn/go-sqlite3。
如果使用场景只是增删改查什么的,问题不会很大,我目前遇到的和 mysql 不兼容的就是create table a like b这种 sql。而且如果不直接执行 sql 而用框架取调用相关函数的话,兼容性会好很多。 八、redis?依赖 很多项目还会依赖redis数据库,那么这种怎么解决依赖问题呢?可以使用miniredis库解决问题:https://github.com/alicebob/miniredis 。miniredis是一个纯GO写的测试用的redis服务,它支持绝大多数redis命令,具体可以看项目介绍。使用起来很简单,直接调用Run函数启动一个测试服务,服务对象的Addr()方法返回服务连接地址。接下来可以就可以拿着这个地址替换当前依赖了。
九、执行测试用例前后设置 有时需要在测试之前或之后进行额外的设置(setup)或拆卸(teardown),为此testing包提供了TestMain函数作为测试文件的入口。如下所示,该文件的测试用例都会在m.Run里运行,如果成功返回0否则非零,因此可以判断执行是否成功。值得注意的是最后应该使用code作为os.Exit参数退出。
十、忽略指定目录 有时需要忽略指定目录,例如自动生成的桩代码,proto文件等,以提高覆盖率,那么对于下面的测试命令:go test -v -covermode=count -coverprofile=coverage_unit.out '-gcflags=all=-N -l' ./... 如果要忽略掉mockdata目录的话,后面加上grep -v mockdata即可:
然后可以运行go tool cover -html=coverage_unit.out -o cover.html,生成网页版报告,查看覆盖率情况。当然还有一个比较tricky的方法,如果生成的桩代码仅限于某个包内使用,那么直接把桩代码文件名改成_test.go后缀的就行了。 十一、关于单元测试的思考 1.单测的意义 首先必须承认有了单元测试之后,增加了代码质量的保障。而且在做修改和重构的时候,也能降低心智负担,相信大家都体验过对一堆没有单测的代码做修改时心里都会有点打怵,生怕改出什么问题。
|
|
开发测试 最新文章 |
pytest系列——allure之生成测试报告(Wind |
某大厂软件测试岗一面笔试题+二面问答题面试 |
iperf 学习笔记 |
关于Python中使用selenium八大定位方法 |
【软件测试】为什么提升不了?8年测试总结再 |
软件测试复习 |
PHP笔记-Smarty模板引擎的使用 |
C++Test使用入门 |
【Java】单元测试 |
Net core 3.x 获取客户端地址 |
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/17 20:36:49- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |