作者:faithchen,腾讯PCG测试开发工程师

  • 一、背景及概要
  • 二、用例编写
  • 三、用例执行
  • 四、结果分析
  • 五、总结
  • 六、其他
一、背景及概要1.1 背景

随着业内对研发效能的重视,DevOps理念愈发地渗入到日常软件研发流程中,我们希望通过自动化测试等手段,能够更加快捷、频繁和可靠地构建、测试、发布软件。

在这个背景下,PCG后台自动化测试平台(同源)为后台单元测试落地到devops流程提供了一站式解决方案。从单测用例编写、用例执行到结果分析,平台都有一套已经在实践中的方案,致力于提升单测编写效率以及提供CI/CD流程高效接入单测能力,保证产品质量,提升研发效能。

本文主要以Go语言为例进行说明,对于Python、C++等语言也有相应的支持。

1.2 后台单测概要

后台测试主要有单元测试接口测试集成测试压力测试。无论哪一种测试,最终目的都只有一个,就是提升代码质量,持续交付。PCG后台自动化测试平台提供了整套符合DevOps规范的单元测试、接口测试、集成测试和压测自动化方案:同源同库、go test可直接运行、MR质量红线等。本文主要介绍***后台单元测试***相关的工具和概念。

说到测试用例(包括单元测试),无外乎三个步骤:用例编写->用例执行->结果分析,接下来对这三个步骤展开说明。

二、用例编写

当需要着手去写用例时,首先要明确一些单元测试的概念。众所周知,单元测试(Unit Testing)本身就是针对软件设计最小可测单位即程序模块的测试,在后台测试中,这个最小模块一般是一个函数方法。

最理想的情况是要测试的单元,不需要模拟对象(mock),也不需要依赖数据库、Redis等组件便可以编写用例去保证代码的功能与质量。举个最简单的例子:

// Add 对两个int类型进行相加func Add(a, b int) int { return a + b}// TestAdd 用于Add函数逻辑是否正确func TestAdd(t *testing.T) { assert.Equal(t, 3, Add(1, 2))//断言}

当然了,必要情况可以多设计一些边界值和异常值来保证功能的完整,这里只是举个最简单的例子。

然而我们很快就会发现,并不是所有情况都那么理想,比如说我突然想看这个函数处理异常时功能是否符合预期,但是这个异常情况通过正常运行并不能或难以复现,或者需要更测试某个条件/逻辑分支,那么这个时候就需要用到mock工具了。

2.1 用例编写之Mock

Mock能帮助解决一部分的单元测试编写问题,那么是哪一部分呢?这部分我们给个名字的话,可以暂时称为无外部依赖单测用例。先上个图:

打开网易新闻 查看精彩图片

从图中可以看到,需要测试的函数方法,只依赖被测代码,不需要其他数据库等,并且在同进程内测试,当然了对外部依赖就更加禁止了。

对于这部分代码的单测,除了可以直接测试的代码外,其他不能直接测试的代码那就需要使用Mock方法。

基于公司目前内部没有一款自己维护的适合公司业务迭代的后台mock框架,众多项目采用外界开源的gomonkey框架进行函数的mock,因其存在一些的稳定性的bug,不支持异包私有函数mock,同包私有方法mock等等问题,在PCG后台自动化测试中,提供了goom mock工具,其提供了针对Go语言的mock能力。

其主要功能特性有:

  1. 私有(未导出)函数(或方法)的mock, 普通函数的mock
  2. mock过程中回调原函数(线程安全, 支持并发单测),已升级为trampoline模式
  3. 异常注入,对函数调用支持异常注入,延迟模拟等稳定性测试
  4. 支持interface mock (面向接口编程和测试,验证中)

假设我们有以下待单测函数:

func foo(i int) int { return i}

对上面foo函数进行mock的示例代码如下:

func TestGoomMock(t *testing.T) { // 创建当前包的mocker mock := mocker.Create() // 完成这个单测以后重置mock defer mock.Reset() // mock函数foo并设定返回值为1 mock.Func(foo).Return(1) // 断言 assert.Equal(t, foo(100), 1)}

如上面代码所示,我们很简单地就对一个函数进行了mock,并对其进行了断言,完成了一个最简单的单元测试 : ) 除了上面设置返回值的方法外,我们也可以通过设置代理函数的方法来mock:

func TestGoomMock(t *testing.T) { mock := mocker.Create() defer mock.Reset() // mock函数foo并设置其代理函数 mock.Func(foo).Apply(func(i int) int { return i * 2 }) assert.Equal(t, foo(100), 200)}

值得提醒的是,在写代码单测过程中,尽量遵循“ 非必须不使用mock ”的原则,以便更好地提高代码覆盖率。

2.2 用例编写之组件依赖

在后台单元测试中,还会遇到一种情况,单纯使用mock难以满足编写用例需求,比如部分代码需要与数据库、Redis、Kafka等组件进行交互,难以编写单元测试用例。这种情况我们将原本需要远程依赖的组件,通过本地提供相关组件的方式,使原本需要通过接口或集成测试实现的用例可以通过单元测试实现,如下图:

打开网易新闻 查看精彩图片

这类单元测试用例注重服务内部模块间的集成验证,除依赖被测代码以外,还可以依赖本地资源。(“本地”即访问网络为localhost,依赖的本地资源包括本地数据库、本地文件系统、同进程mock或者本地测试替身等)。在PCG自动化分层定义中,将这类测试用例称为A1单元测试用例,前文提到的不需要依赖本地资源的称为A0单元测试用例。 对于上述这类单测,提供了以下方案,解决开发阶段需要在 蓝盾流水线本地 两种模式下编写和运行含本地依赖测试用例的难题,简化开发蓝盾流水线配置和本地配置成本,覆盖司外mysql、redis、kalfka、es和司内dcached的数据库相关组件。

2.2.1 实现方案

假设有以下mysql的业务逻辑,需要与mysql进行交互:

// InsertNameAndAge 在数据中插入name和agefunc InsertNameAndAge(db *sql.DB, name string, age string) { sqlStr := "insert into user(name, age) values (?,?)" ret, err := db.Exec(sqlStr, name, age) if err != nil { return }}

对于上述代码的单测,目前的方案是通过docker-compose工具,使用 docker-compose.yml 定义单测需要的组件服务,例如下图的yml文件配置了一个mysql组件服务:

version: "3"services: mysql: image: mysql:5.6 container_name: ngtest-mysql ports: - 4306:3306 environment: - MYSQL_DATABASE=test - MYSQL_ROOT_PASSWORD=abcde

接下来通过代码完成以下流程:

  1. 在运行单测前使用docker-compose命令在后台启动组件 docker-compose -f docker-compose.yml up -d
  2. 执行单测用例
  3. 最后销毁本地组件 docker-compose -f docker-compose.yml down

具体代码:

// TestMain 运行单测环境设置func TestMain(m *testing.M) { InitDockerYml() //在用例执行前通过docker-compose命令在后台启动组件 m.Run() //运行用例 TearDownDocker()//运行用例后销毁组件 os.Exit(0)}// TestInsertNameAndAge 测试InsertNameAndAge功能是否正常func TestInsertNameAndAge(t *testing.T){ //通过本地yml文件获取mysql配置,并且获取默认DB db, err := service.GetMySQLDB(s.MySQLConfig, "") assert.Nil(t, err) InsertNameAndAge(db, "Tom", 11) var name string var age uint8 //获取最后插入数据的自增ID id, err := result.LastInsertId() err = db.QueryRow("select name,age from test where id = ?", id).Scan(&name, &age) //断言 assert.Nil(t, err) assert.EqualValues(t, "Tom", name) assert.EqualValues(t, 11, age)}

这部分功能还在持续迭代和完善中,后续会提供更详细文档。

2.3 用例编写共识及规范2.3.1 用例编写共识

这里简要介绍一些有较多测试经验的工程师总结的一些测试编写共识,有助于写出更高质量的测试代码。下面简要列出几点:

  • 非必要不使用mock:这一点在上面已经简单的提到了,过多的mock会降低单测的有效性,所以应该减少mock,非必要时不使用mock,来起到单测应该发挥的作用。
  • 在单测中不允许使用Sleep:可能有部分人员在编写测试用例的时候,对一些有异步逻辑的方法测试时,会使用sleep方法来保证用例可以通过,但是这与单元测试的“小而快”的理念相悖,容易带来大量的单测运行耗时,减慢我们mr等操作的进度。
  • 避免脆弱的测试:编写测试用例是为了保证代码质量,并且希望一旦出现失败是代码变动导致了bug才使用例失败。但是如果由于一些偶现的环境因素或者其他原因导致用例失败,让我们需要投入人力去排查,那么这样的测试用例是应该被整改或者删除的,因为它给了我们错误的信息,并且花费了我们不必要的时间。
  • 提高用例可操作性:即如果用例失败,我们能够快速定位错误的位置。
三、用例执行3.1 本地执行后台单测用例

大家知道Go语言是自带一套单元测试和性能测试系统。当写完本地的后台单元测试之后,可以通过以下命令运行,并获取结果:

go test -v `go list ./...|grep -v apitest|grep -v api_test` -gcflags=all=-l -covermode count -coverpkg ./... -coverprofile cover.out

其中grep -v apitest|grep -v api_test是将命名表示为接口api测试的测试用例剔除,gcflags=all=-l表示禁止内联,防止我们一些mock的函数不生效,并且生成cover.out文件。

控制台输出的信息如下:

2021-05-17 14:32:09.568 INFO automaxprocs@v1.3.0/automaxprocs.go:32 maxprocs: Leaving GOMAXPROCS=8: CPU quota undefined=== RUN TestSayHello--- PASS: TestSayHello (0.00s)=== RUN TestSayHi--- PASS: TestSayHi (0.00s)PASScoverage: 10.1% of statements in ./...ok trpc.app.Greeter/greeter 0.010s coverage: 10.1% of statements in ./...? trpc.app.Greeter/pb [no test files]

里面可以看到每个用例的成功与失败,还有覆盖率。 cover.out文件的输出如下:

mode: counttrpc.app.Greeter/pb/helloworld.pb.go:31.40,31.63 1 0trpc.app.Greeter/pb/helloworld.pb.go:32.40,32.77 1 0trpc.app.Greeter/pb/helloworld.pb.go:33.41,33.42 0 0trpc.app.Greeter/pb/helloworld.pb.go:34.51,36.2 1 1trpc.app.Greeter/pb/helloworld.pb.go:38.54,40.2 1 0trpc.app.Greeter/pb/helloworld.pb.go:41.82,43.2 1 03.2 CI/CD执行后台单测用例

编写单测的最终目的还是能够在CI/CD之间发挥作用,提升代码的质量。

蓝盾流水线添加后台单测插件

如下图所示,添加了同源自动化测试插件后,设置代码路径code_path,和分支code_tag后,如下图勾选单元测试便可以执行对应的单元测试(当然,如果有需要也可以加上接口测试)。

打开网易新闻 查看精彩图片

执行完之后,在插件日志中,也可以看到自动化报告:

打开网易新闻 查看精彩图片

腾讯应用宝、微视、看点、腾讯视频等PCG大部分业务都在使用。

四、结果分析4.1 结果报告

在上述执行完单测后,我们便可以得到测试结果。在同源自动化测试插件的日志中我们可以看到相应结果的链接:

自动化报告信息如下:

打开网易新闻 查看精彩图片

这个报告能够很直观的告诉我们本地单测的执行情况,这个在单测数目比较多的时候回更加直观与高效,帮助我们查看用例的执行情况,当某个用例出错时,也会相应地列出出错原因等,便于定位,例如:

打开网易新闻 查看精彩图片

覆盖率报告信息如下:

打开网易新闻 查看精彩图片

4.2 质量红线

在获取了单测结果报告后,在代码commit、mr等关键环节中,我们还可以进一步设置质量红线,我们提供了同源后台质量红线插件,大家可以在上面配置代码覆盖率用例通过率等红线,保证我们进行代码mr等关键环节的代码质量。

五、总结

单测的编写及使用流程如下:

打开网易新闻 查看精彩图片

CI/CD的接入流程如下:

打开网易新闻 查看精彩图片

六、其他

其他语言如Python、C++的后台单元测试,平台也有相应多的支持。 有任意疑问或者建议随时欢迎联系讨论。

福利时间

抽5位朋友送出《小程序开发原理与实战》,腾讯一线专家带你学习小程序开发,干货满满!戳这里: 福利 参与!

打开网易新闻 查看精彩图片