Go 单元测试

单元测试属于保障代码质量的手段,一些公司还会拿单测覆盖率来做员工考核,写好单测对个人变的很重要。究其原因,现在的互联网属于存量时代,稳定性排在第一位。

但单元测试也并不好些,业务上总会依赖各种各样的中间件、第三方付。一般我们都在日常环境执行单测,但日常环境并不稳定,中间件的数据有时还是缺失的,直接写的单测用例很难正常执行,即使现在能正常执行,保不齐哪天又不能正常执行了。

在正式执行单测之前,可以利用 TestMain 将中间件依赖进行初始化,TestMain 会在执行单测用例之前首先被执行,在执行完所有单测用例之后,TestMain 重新执行去回收初始化的资源。

单测标准

单测体系标准中描述了集成测试的三个优势:Quality、Productivity、Flexibility,个人对 Productivity 是非常认同的,bug 最好在单元测试阶段被发现和解决,留个人工测试的最好是更复杂的业务逻辑。

在这里插入图片描述
上图中,单元测试是最基础的,相当于 respect to time,它不需要对三方有任何依赖,只关注独立模块的正确性;集成测试则上了一个“台阶”,它需要和三方接口或者底层中间件做真实交互,来保证功能的正确性;GUI 测试代价最高,它完全依赖测试通过 APP 界面来进行测试。

关于什么是好的单测,有下面几个观点:

  1. 简短,单测用例代码易读、易理解,有一个清晰的验证目标,并且仅仅只做一个目标的评估。
  2. 简单,单测依赖的注入和销毁要足够简单。对于单测执行的本地上下文依赖,要有通用的集成方式、方法,单测写起来很简单,不要考虑乱七八糟的环境、配置等问题。
  3. 执行要快,整个单测的运行时间要足够快,如果执行单测需要花费很长的时间,势必会影响开发者执行单测的意愿
  4. 标准,代码风格统一

依赖隔离

单测方法中是否要真实查询底层数据、请求第三方接口非常重要,一个很现实的问题:如果不真实请求第三方或者查询底层数据,单测逻辑无法执行。

从我个人的经验来说,单测方法中最好不要真实依赖第三方(第三方服务或者中间件),主要原因是单测环境下的第三方不能保证稳定。之前好不容易搞好的单测,下次重新运行单测却失败了,排查问题的原因又很繁琐,开发效率上大打折扣。

概括的说,这是单测的可重复性问题:

如果单测对外部环境(网络、中间件)有依赖,很容易导致持续集成不可用,排查单测不可用的原因也很花费精力。单测需要是可重复执行的,需要通过mock的手段来屏蔽外部依赖

市面上有很多 mock 的方式方法,但 mock 也是需要条件的,为了能顺利 mock 逻辑,大多数情况下还需要对现有代码做逻辑调整。

依赖 mock

示例

可以使用 gomonkey 直接 mock 方法实现,上手比较简单,说明文档中只是简单的阐述了参考 Monkey Patching in Go 的实现。

首先,mock 一个结构体中的方法。特别提醒,mock 的方法针对的是结构体的类型,和结构体具体的值没有关系。在 mock 生效期间,只要实例化这个结构体类型,方法都是被mock的实现。

假设调用第三方依赖的场景,QueryUrl 实际会调用第三方接口,但第三方接口没有测试环境,我们本地无法执行单元测试,我们就可以 mock Client 类型下的 QueryUrl 方法,让这个方法不执行实际请求,而是直接返回一个固定的值。

type Client struct {
}

func (c *Client) QueryUrl(ctx context.Context, header http.Header, id string) (int, error) {
	return 1, nil
}

下面是使用 gomonkey mock之后的调用,mock 方法中将返回的值修改为2,最后对返回的值进行断言。

func TestClient_QueryUrl(t *testing.T) {
	var c *Client
	patches := gomonkey.ApplyMethodFunc(c, "QueryUrl", func(ctx context.Context, header http.Header, id string) (int, error) {
		return 2, nil
	})
	defer patches.Reset()

	client := new(Client)
	code, _ := client.QueryUrl(context.TODO(), nil, "i")
	assert.Equal(t, code, 2)
}

gomonkey

gomonkey用法比较简单,常规提供了 func 和 method 的 mock 方法,针对不同的单测场景,可以选择不同的 mock 方法,比较常用的有:

  • func (this *Patches) ApplyFunc(target, double interface{}) *Patches
  • func (this *Patches) ApplyFuncReturn(target interface{}, returns …interface{}) *Patches

也无需担心函数的用法,在工程的项目下提供了详细的单测示例:

在这里插入图片描述

使用这个库最困扰的地方在于本机环境执行报错 permission denied,很揪心,有时候并不好解决,比如苹果M2芯片。不过,Doc下的特别声明也需要我们认真对待:

在这里插入图片描述
使用 gomonkey 的过程中可能会遇到非 debug 模式执行失败的问题,就是第一项补充说明的内容。gomonkey 执行需要禁用内联优化,可以通过在命令行指定 -gcflags=all=-l 来解决。还有一个取巧的办法,将函数体声明为 //go:noinline,不过,这种方式在单测结束后最好修改回来。

第二项是说 gomonkey 不是并发安全的,访问其他 go 协程内的 patch 的方法可能会发生 panic。

关于 Doc 中声明的 Supported Platform ,ARCH 支持的版本中包含 - amd64 和 - arm64,OS 中支持了 - macos X,但本地机器是否在这个支持的体系中,需要自己查看,新的系统大概率都不支持。

在这里插入图片描述

临时的解决方案

网上有一套现成的解决方案,但并不能解决所有问题,我们可以尝试看看效果,链接地址:macos-golink-wrapper,用来解决 syscall.Mprotect panic: permission denied 的问题。

找到本地 go 的安装目录,然后按照下面的 4 个步骤执行:

  1. 打开文件: /go安装目录下/pkg/tool/darwin_amd64
  2. 重命名文件 link to original_link
  3. 下载 link wrapper 替换之前的 link
  4. 确保新的 link 有可执行权限: chmod +x link

Go单元测试

当直接使用IDE进行单元测试时,有没有好奇它时如何实现的?比如GoLand写的测试用例。

我们创建一个简单的测试用例,来解释一下基本的测试过程。go 语言中的测试用例文件需要使用 _test 作为文件后缀。我们创建了文件名称为 live_test.go 的文件,在项目的 server 目录下。

package server

import "testing"

func TestLive(t *testing.T) {
    t.Log("this is go test")
}

在命令行,切换到 server 目录下就可以执行 go 的测试指令。go test 会默认使用当前的目录,如果想在 server 之外的其他目录执行 go test 指令,需要明确指定具体路径。

➜  server go test
PASS
ok      test/server    0.301s

测试用例中,虽然打印了程序的执行日志,但输出结果中并没有显示。怎么样才能显示控制台的输出呢?在命令行追加 -v 标志来获取更详细的信息。跟其他命令一样,-v 用来标识 verbose 冗余输出。

➜  server go test -v
=== RUN   TestLive
--- PASS: TestLive (0.00s)
    live_test.go:6: this is go test
PASS
ok      test/server    0.415s

对比 -v 追加的前后输出,确实显示了很多额外的输出。之前都不知道运行了哪些测试用例,现在通过 PASS 标志可以清楚的看到。

除了验证程序运行的输出结果是否正确外,go 还提供了测试的覆盖率数据。同上,我们追加覆盖率标识 -cover 来获知测试用例对代码的覆盖率。

我们稍稍改动一下代码,在 server 包中的 live 文件下声明一个方法,供测试用例中做单元测试。方法如下,之所以使用 switch 语句,主要是想说明 go 中覆盖率的计算。

package server

func GetMode(t int) int {
    switch t {
    case 1:
        return 1
    case 2:
        return 2
    default:
        return 3
    }
}

我们对测试代码做一些调整,保证所有的代码分支都被完全覆盖,看一下 go 的输出结果。

下面是对测试代码所做的改动,在测试用例中调用 GetMode 的方法,替换之间仅仅打印日志的代码逻辑

package server

import "testing"

func TestLive(t *testing.T) {
    GetMode(1)
    GetMode(2)
    GetMode(3)

    t.Log("this is go test")
}

所有的代码分支都已经覆盖到了,我们执行单元测试,并输出覆盖率的结果。结果如下:

➜  server go test -v -cover
=== RUN   TestLive
--- PASS: TestLive (0.00s)
    live_test.go:10: this is go test
PASS
coverage: 100.0% of statements
ok      test/server    0.418s
➜  server
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值