目录
单元测试的概念
什么是单元测试
为什么要写单元测试
编写测试用例的原则
Go约定的测试规则
单元测试的实践过程
编写一个简单的测试用例
真实案例
被测试的函数
简单的测试
多维度测试案例
GoFrame 中依赖框架配置文件的单元测试
加载测试用例的配置文件
引入环境,并编写mysql、redis的测试用例
第三方测试框架 (后续再补充~)
参考文档
Gopher一定要养成写单元测试的习惯,这样才能保证我们交付代码的质量,同时提升个人开发水平!
1. 最小的可测试单位,比如函数、对象的某个接口
2. 是软件开发过程中对最小单位进行正确性验证的测试工作
1. 保证变更、重构的正确性,特别是在多人协作的项目中
2. 简化调试过程:可以快速定位到问题代码
3. 单元测试是最好的开发文档:单元测试给出具体函数的使用方法及边界值,是最好的示例代码
1. 单一原则:一个测试用例只负责一个应用场景
2. 原子性:结果只有两种情况:Pass/Fail
3. 优先级:核心的组件、逻辑要优先测试
4. 公共使用库: utils工具类库要重点覆盖
1. 单元测试的文件需要和被测试的文件在统一目录下,测试用例名称必须是xxx_test.go的格式
测试文件中包含三种类型的函数,单元测试函数、基准测试函数和示例函数。
类型 | 格式 | 作用 |
单元测试函数 | 函数名前缀为Test | 测试程序的逻辑行为是否达到预期 |
基准函数 | 函数名前缀为Benchmark | 测试函数的性能 |
示例函数 | 函数名前缀为Example | 为doc文档提供示例 |
package service
import "testing"
func add(a, b int) int {
return a + b
}
func TestAdd(t *testing.T) {
a := 10
b := 20
wanted := 30
actual := add(a, b)
if wanted != actual {
t.Errorf("[add函数的入参:%d %d] [期望值:%d] [实际结果:%d]", a, b, wanted, actual)
}
}
执行当前单元测试的命令: go test -v .\user_test.go
PS C:\Program Files\JetBrains\GoRepository\gf-demo-user\internal\service> go test -v .\user_test.go
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok command-line-arguments 0.075s
执行当前目录下所有的测试用例(跳过某个测试函数),并导出到制定的输出文件:go test -v ./... --short=true -coverprofile cover.out
PS C:\Program Files\JetBrains\GoRepository\gf-demo-user\internal\service> go test -v ./... --short=true -coverprofile cover.out
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
coverage: 0.0% of statements
ok github.com/gogf/gf-demo-user/v2/internal/service 0.121s coverage: 0.0% of statements
查看输出覆盖率的html文档:go tool cover --func=cover.out
PS C:\Program Files\JetBrains\GoRepository\gf-demo-user\internal\service> go tool cover --func=cover.out
github.com/gogf/gf-demo-user/v2/internal/service/biz_ctx.go:22: BizCtx 0.0%
github.com/gogf/gf-demo-user/v2/internal/service/biz_ctx.go:29: RegisterBizCtx 0.0%
github.com/gogf/gf-demo-user/v2/internal/service/middleware.go:19: Middleware 0.0%
github.com/gogf/gf-demo-user/v2/internal/service/middleware.go:26: RegisterMiddleware 0.0%
github.com/gogf/gf-demo-user/v2/internal/service/session.go:21: Session 0.0%
github.com/gogf/gf-demo-user/v2/internal/service/session.go:28: RegisterSession 0.0%
github.com/gogf/gf-demo-user/v2/internal/service/user.go:26: User 0.0%
github.com/gogf/gf-demo-user/v2/internal/service/user.go:33: RegisterUser 0.0%
total: (statements) 0.0%
func Split(s, sub string) (result []string) {
if len(strings.TrimSpace(s)) == 0 {
return
}
i := strings.Index(s, sub)
for i > -1 {
if i == 0 {
s = s[len(sub):]
} else {
result = append(result, s[:i])
s = s[i+len(sub):]
}
i = strings.Index(s, sub)
}
result = append(result, s)
return
}
get := Split("a:b:c", ":")
wanted := []string{"a", "b", "c"}
if !reflect.DeepEqual(wanted, get) {
t.Errorf("expected:%v,get:%v", wanted, get)
}
func TestMultiSplit(t *testing.T) {
type test struct {
name string
input string
sub string
wanted []string
}
tests := []test{
{name: "正常的测试用例", input: "a:b:c", sub: ":", wanted: []string{"a", "b", "c"}},
{name: "字符串头位置匹配测试", input: ":a:b:c", sub: ":", wanted: []string{"a", "b", "c"}},
{name: "无匹配项的测试", input: "abc", sub: ":", wanted: []string{"abc"}},
{name: "英文串的测试", input: "abcd", sub: "bc", wanted: []string{"a", "d"}},
{name: "中文串的测试", input: "云原生", sub: "云", wanted: []string{"原生"}},
}
for _, d := range tests {
get := Split(d.input, d.sub)
if !reflect.DeepEqual(d.wanted, get) {
t.Errorf("[testName:%v][expected:%v] [get:%v]", d.name, d.wanted, get)
}
}
}
root.go
package testingutil
import (
"path"
"runtime"
)
func RootDir() string {
_, d, _, _ := runtime.Caller(0)
return path.Dir(path.Dir(d))
}
root_test.go
package testingutil
import "testing"
func TestRootDir(t *testing.T) {
rootDir := RootDir()
t.Log(rootDir)
}
func BenchmarkRootDir(b *testing.B) {
for i := 0; i < b.N; i++ {
RootDir()
}
}
初始化加载GoFrame配置环境
config.go
package testingutil
import (
"github.com/gogf/gf/v2/os/genv"
"path"
)
func init() {
InitConfig()
}
func InitConfig() {
configDir := path.Join(RootDir(), "manifest/config")
genv.Set("GF_GCFG_PATH", configDir)
}
func TestMysql(t *testing.T) {
testingutil.InitConfig()
ctx := gctx.New()
countSql := "select collection_id,count(1) as count from transfer_logs" + " where 1 = 1" + " " + " group by collection_id"
var countList []struct {
CollectionId int64 `json:"collection_id"`
Count int `json:"count"`
}
// 获取内容
all, err := g.DB().Ctx(ctx).GetAll(ctx, countSql)
if err != nil {
t.Fatal(err)
}
all.Structs(&countList)
fmt.Printf("%+v", countList)
}
func TestRedis(t *testing.T) {
testingutil.InitConfig()
ctx := gctx.New()
cache := gcache.NewAdapterRedis(g.Redis())
err := cache.Set(ctx, 1, 1, time.Second*10)
if err != nil {
t.Fatal(err)
}
}
1. GoConvey测试框架
2. testify
GO单元测试
go test命令(Go语言测试命令)完全攻略