软件测试
程序开发过程中出错是家常便饭,不仅仅是单个模块、函数可能出错,不同函数、微服务之间的协作也会经常产生 bug,而且更加隐蔽、难以排查。代码的错误轻则耽误开发时间,扰乱流程推进,重则导致系统故障,造成严重的利益损失。因此,我们要在日常的开发中做好尽可能完备的测试,它是避免事故的最后一档屏障。
按照开发阶段划分,测试可分为:
- 单元测试:又称模块测试,针对软件设计中的最小单位——程序模块(单独的函数、模块),进行正确性检查的测试工作。
- 集成测试:又叫组装测试,通常在单元测试的基础上,将所有程序模块进行有序的、递增测试。重点测试不同模块的接口部分。
- 系统测试:指将整个软件系统看成一个整体进行测试,包括对功能、性能以及软件所运行的软硬件环境进行测试。系统测试在系统集成完毕后进行测试,前期主要测试系统的功能是否满足需求,后期主要测试系统运行的性能是否满足需求,以及系统在不同的软硬件环境中的兼容性等。
这三者按照单元、集成、系统测试的顺序,覆盖率逐层变小,成本逐层增加,单元测试的覆盖率一定程度上决定了代码的质量。本文主要讨论单元测试。
Golang 单元测试
单元测试主要包括输入、测试单元、输出以及校对,单元的概念比较广,包括接口、函数、模块等,用校对来保证代码的功能与我们的预期相符。单元测试一方面可以保证质量,在整体覆盖率足够的情况下,一定程度上保证新功能的正确性,又未破坏原有代码的正确性;另一方面,单元测试可以提升效率,在代码有漏统的情况下,通过编写单元测试,可以在一个较短周期内定位和修复问题。
规则
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 机制。
mock/stub 测试,当待测试的函数/对象的依赖关系很复杂,并且有些依赖不能直接创建,例如数据库连接、文件I/O等。这种场景就非常适合使用 mock/stub 测试。简单来说,就是用 mock 对象模拟依赖项的行为。
基准测试
当我们尝试去优化代码的性能时,首先得知道当前的性能怎么样。Go 语言标准库内置的 testing 测试框架提供了基准测试(benchmark)的能力,能让我们很容易地对某一段代码进行性能测试。