Golang 单元测试 | 青训营

59 阅读4分钟

软件测试

程序开发过程中出错是家常便饭,不仅仅是单个模块、函数可能出错,不同函数、微服务之间的协作也会经常产生 bug,而且更加隐蔽、难以排查。代码的错误轻则耽误开发时间,扰乱流程推进,重则导致系统故障,造成严重的利益损失。因此,我们要在日常的开发中做好尽可能完备的测试,它是避免事故的最后一档屏障。

按照开发阶段划分,测试可分为:

  • 单元测试:又称模块测试,针对软件设计中的最小单位——程序模块(单独的函数、模块),进行正确性检查的测试工作。
  • 集成测试:又叫组装测试,通常在单元测试的基础上,将所有程序模块进行有序的、递增测试。重点测试不同模块的接口部分。
  • 系统测试:指将整个软件系统看成一个整体进行测试,包括对功能、性能以及软件所运行的软硬件环境进行测试。系统测试在系统集成完毕后进行测试,前期主要测试系统的功能是否满足需求,后期主要测试系统运行的性能是否满足需求,以及系统在不同的软硬件环境中的兼容性等。

这三者按照单元、集成、系统测试的顺序,覆盖率逐层变小,成本逐层增加,单元测试的覆盖率一定程度上决定了代码的质量。本文主要讨论单元测试。

Golang 单元测试

unit-test.png

单元测试主要包括输入、测试单元、输出以及校对,单元的概念比较广,包括接口、函数、模块等,用校对来保证代码的功能与我们的预期相符。单元测试一方面可以保证质量,在整体覆盖率足够的情况下,一定程度上保证新功能的正确性,又未破坏原有代码的正确性;另一方面,单元测试可以提升效率,在代码有漏统的情况下,通过编写单元测试,可以在一个较短周期内定位和修复问题。

规则

Golang 有内置的 testing 包来帮助进行单元测试,它有一套规则:

  • Golang 中有个约定,就是把测试文件和代码放在同一个文件夹内,并且测试文件的命名要以 _test.go 后缀结尾。
❯ tree
.
├...
│   └── service
│       ├── publish_post.go
│       ├── publish_post_test.go
│       ├── query_page_info.go
│       └── query_page_info_test.go
  • 测试用例以 Test 开头,并且以 testing.T 作为输入参数,使用这个 T 对象来管理测试状态。
  • 初始化逻辑放到 TestMain
package service

import (
	"community_topic_backend/biz/dal"
	"os"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestMain(m *testing.M) {
	dal.Init("../data/")
	os.Exit(m.Run())
}
func TestQueryPageInfo(t *testing.T) {
	pageInfo, _ := QueryPageInfo(1)
	assert.NotEqual(t, nil, pageInfo)
	assert.Equal(t, 5, len(pageInfo.PostList))
}

执行测试

❯ go test -v ./biz/service
=== RUN   TestPublishPost
=== RUN   TestPublishPost/测试发布回帖
--- PASS: TestPublishPost (0.00s)
    --- PASS: TestPublishPost/测试发布回帖 (0.00s)
=== RUN   TestQueryPageInfo
--- PASS: TestQueryPageInfo (0.00s)
PASS
ok  	community_topic_backend/biz/service	1.497s

覆盖率

三问:

  • 如何衡量代码是否经过了足够的测试?
  • 如何评价项目的测试水准?
  • 如何评估项目是否达到了高水准测试等级?

答:代码覆盖率。

go test -v ./biz/service -cover
=== RUN   TestPublishPost
=== RUN   TestPublishPost/测试发布回帖
--- PASS: TestPublishPost (0.00s)
    --- PASS: TestPublishPost/测试发布回帖 (0.00s)
=== RUN   TestQueryPageInfo
--- PASS: TestQueryPageInfo (0.00s)
PASS
	community_topic_backend/biz/service	coverage: 80.9% of statements
ok  	community_topic_backend/biz/service	0.320s	coverage: 80.9% of statements

Mock

mock.png

工程中的复杂项目,常常依赖网络请求、文件读写等操作,这样很难保证每一次的外部输入是相同的,这对测试很不利。单元测试需要不保证稳定性和幂等性:

  • 稳定性指相互隔离,任何时间、任何环境都能运行测试,不受其他因素影响。
  • 幂等性是指每一次测试应该与之前产生相同结果。

实现这两个目标用到 mock 机制。

mock/stub 测试,当待测试的函数/对象的依赖关系很复杂,并且有些依赖不能直接创建,例如数据库连接、文件I/O等。这种场景就非常适合使用 mock/stub 测试。简单来说,就是用 mock 对象模拟依赖项的行为。

可以用框架 gomonkeygomock

基准测试

当我们尝试去优化代码的性能时,首先得知道当前的性能怎么样。Go 语言标准库内置的 testing 测试框架提供了基准测试(benchmark)的能力,能让我们很容易地对某一段代码进行性能测试。

参考

testing-go-project-root

testing

Go 语言入门 - 工程实践