Go 单测入门篇:Golang 单元测试基本使用

简介: Go 单测入门篇:Golang 单元测试基本使用

Go 单测入门篇:Golang 单元测试基本使用

Golang 单元测试规范

Go 单元测试概要

Go 语言的单元测试默认采用官方自带的测试框架,通过引入 testing 包以及 执行 go test 命令来实现单元测试功能。

在源代码包目录内,所有以 _test.go 为后缀名的源文件会被 go test 认定为单元测试的文件,这些单元测试的文件不会包含在 go build 的源代码构建中,而是单独通过 go test 来编译并执行。

Go 单元测试的基本规范

Go 单元测试的基本规范如下:

  • • 每个测试函数都必须导入 testing 包。测试函数的命名类似func TestName(t *testing.T),入参必须是 *testing.T
  • • 测试函数的函数名必须以大写的 Test 开头,后面紧跟的函数名,要么是大写开关,要么就是下划线,比如 func TestName(t *testing.T) 或者 func Test_name(t *testing.T) 都是 ok 的, 但是 func Testname(t *testing.T)不会被检测到
  • • 通常情况下,需要将测试文件和源代码放在同一个包内。一般测试文件的命名,都是 {source_filename}_test.go,比如我们的源代码文件是allen.go ,那么就会在 allen.go 的相同目录下,再建立一个 allen_test.go 的单元测试文件去测试 allen.go 文件里的相关方法。

当运行 go test 命令时,go test 会遍历所有的 *_test.go 中符合上述命名规则的函数,然后生成一个临时的 main 包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。

从一个简单测试用例来确认 go test 的各种使用方法

一个简单的 xxx_test.go 的单元测试文件如下,里面有两个测试方法:

package util
import (
    "testing"
)
func TestSum(t *testing.T) {
    if Sum(1, 2, 3) != 6 {
        t.Fatal("sum error")
    }
}
func TestAbs(t *testing.T) {
    if Abs(5) != 5 {
        t.Fatal("abs error, except:5, result:", Abs(5))
    }
}

go test -v 执行单测并打印详情

运行方法:进入到包内,运行命令 go test -v ,参数 -v 可以打印详情。 也可以只运行某个方法的单元测试: go test -v -run="xxx" ,支持正则表达式。

allen@MackBook:~/work/goDev/Applications/src/baseCodeExample/gotest$go test -v
=== RUN   TestSum
--- PASS: TestSum (0.00s)
=== RUN   TestAbs
--- PASS: TestAbs (0.00s)
PASS
ok   baseCodeExample/gotest 0.005s
allen@MackBook:~/work/goDev/Applications/src/baseCodeExample/gotest$go test -v -run="Abs"
=== RUN   TestAbs
--- PASS: TestAbs (0.00s)
PASS
ok   baseCodeExample/gotest 0.006s

go test -v -cover 执行单测并计算覆盖率

go test 工具还有个功能是测试单元测试的覆盖率,用法为 go test -v -cover, 示例如下:

allen@MackBook:~/work/goDev/Applications/src/baseCodeExample/gotest$go test -v -cover
=== RUN   TestSum
--- PASS: TestSum (0.00s)
=== RUN   TestAbs
--- PASS: TestAbs (0.00s)
PASS
coverage: 85.7% of statements
ok   baseCodeExample/gotest 0.005s

从覆盖率来看(coverage: 85.7% of statements),单元测试没有覆盖全部的代码,只有 85.7% ,我们可以通过如下命令将 cover 的详细信息保存到cover.out 中。

go test -cover -coverprofile=cover.out -covermode=count
注:
-cover 允许代码分析
-covermode 代码分析模式(set:是否执行;count:执行次数;atomic:次数,并发执行)
-coverprofile 输出结果文件

然后再通过

go tool cover -func=cover.out

查看每个方法的覆盖率。

allen@MackBook:~/work/goDev/Applications/src/baseCodeExample/gotest$go tool cover -func=cover.out
baseCodeExample/gotest/compute.go:5: Sum  100.0%
baseCodeExample/gotest/compute.go:13: Abs  66.7%
total:     (statements) 85.7%

这里发现是 Abs 方法没有覆盖完全,因为我们的用例只用到了正数的那个分支。 还可以使用 html 的方式查看具体的覆盖情况。

go tool cover -html=cover.out

会默认打开浏览器,将覆盖情况显示到页面中:

可以看出 Abs 方法的负数分支没有覆盖到。将 TestAbs 方法修改如下即可:

func TestAbs(t *testing.T) {
    if Abs(5) != 5 {
        t.Fatal("abs error, except:5, result:", Abs(5))
    }
    if Abs(-4) != 4 {
        t.Fatal("abs error, except:4, result:", Abs(-4))
    }
}

再次运行:

go test -cover -coverprofile=cover2.out -covermode=count
go tool cover -func=cover2.out

运行结果如下:

allen@MackBook:~/work/goDev/Applications/src/baseCodeExample/gotest$go test -cover -coverprofile=cover2.out -covermode=count
PASS
coverage: 100.0% of statements
ok   baseCodeExample/gotest 0.006s
allen@MackBook:~/work/goDev/Applications/src/baseCodeExample/gotest$go tool cover -func=cover2.out
baseCodeExample/gotest/compute.go:5: Sum  100.0%
baseCodeExample/gotest/compute.go:13: Abs  100.0%
total:     (statements) 100.0%

这个说明已经达到了 100% 的覆盖率了。

Go 单测覆盖度的相关命令汇总如下:

go test -v -cover
go test -cover -coverprofile=cover.out -covermode=count
go tool cover -func=cover.out

Go 单测常见使用方法

测试单个文件

通常,一个包里面会有多个方法,多个文件,因此也有多个 test 用例,假如我们只想测试某一个方法的时候,那么我们需要指定某个文件的某个方法

如下:

allen@MackBook:~/work/goDev/Applications/src/gitlab.allen.com/avatar/app_server/service/centralhub$tree .
.
├── msghub.go
├── msghub_test.go
├── pushhub.go
├── rtvhub.go
├── rtvhub_test.go
├── userhub.go
└── userhub_test.go
0 directories, 7 files

总共有7个文件,其中有三个test文件,如果直接运行 go test,就会测试所有test.go文件了。

但是,假如我们只更新了 rtvhub.go 里面的代码,所以我只想要测试 rtvhub.go 里面的某个方法,那么就需要指定文件,具体的方法就是同时指定我们需要测试的test.go 文件和 它的源文件,如下:

go test -v msghub.go  msghub_test.go

测试单个文件下的单个方法

在测试单个文件之下,假如我们单个文件下,有多个方法,我们还想只是测试单个文件下的单个方法,要如何实现?我们需要再在此基础上,用 -run 参数指定具体方法或者使用正则表达式。

假如 test 文件如下:

package centralhub
import (
    "context"
    "testing"
)
func TestSendTimerInviteToServer(t *testing.T) {
    ctx := context.Background()
    err := sendTimerInviteToServer(ctx, 1461410596, 1561445452, 2)
    if err != nil {
        t.Errorf("send to server friendship build failed. %v", err)
    }
}
func TestSendTimerInvite(t *testing.T) {
    ctx := context.Background()
    err := sendTimerInvite(ctx, "test", 1461410596, 1561445452)
    if err != nil {
        t.Errorf("send timeinvite to client failed:%v", err)
    }
}
只测试 TestSendTimerInvite 方法
go test -v msghub.go  msghub_test.go -run TestSendTimerInvite
测试所有正则匹配 SendTimerInvite 的方法 
go test -v msghub.go  msghub_test.go -run "SendTimerInvite"

测试所有方法

直接 go test 就行

竞争检测(race detection)

go run -race 执行竞争检测

当两个goroutine并发访问同一个变量,且至少一个goroutine对变量进行写操作时,就会发生数据竞争(data race)。 为了协助诊断这种bug,Go提供了一个内置的数据竞争检测工具。 通过传入-race选项,go tool就可以启动竞争检测。

$ go test -race mypkg    // to test the package
$ go run -race mysrc.go  // to run the source file
$ go build -race mycmd   // to build the command
$ go install -race mypkg // to install the package

示例代码

package main
import (
    "fmt"
    "time"
)
func main() {
    var i int = 0
    go func() {
        for {
            i++
            fmt.Println("subroutine: i = ", i)
            time.Sleep(1 * time.Second)
        }
    }()
    for {
        i++
        fmt.Println("mainroutine: i = ", i)
        time.Sleep(1 * time.Second)
    }
}

演示结果

$ go run -race testrace.go
mainroutine: i =  1
==================
WARNING: DATA RACE
Read at 0x00c0000c2000 by goroutine 6:
  main.main.func1()
      /Users/wudebao/Documents/workspace/goDev/Applications/src/base-code-example/system/testrace/testrace.go:12 +0x3c
Previous write at 0x00c0000c2000 by main goroutine:
  main.main()
      /Users/wudebao/Documents/workspace/goDev/Applications/src/base-code-example/system/testrace/testrace.go:18 +0x9e
Goroutine 6 (running) created at:
  main.main()
      /Users/wudebao/Documents/workspace/goDev/Applications/src/base-code-example/system/testrace/testrace.go:10 +0x7a
==================
subroutine: i =  2
mainroutine: i =  3
subroutine: i =  4
mainroutine: i =  5
subroutine: i =  6
mainroutine: i =  7
subroutine: i =  8
subroutine: i =  9
mainroutine: i =  10
相关文章
|
3天前
|
安全 Go
Golang深入浅出之-Go语言模板(text/template):动态生成HTML
【4月更文挑战第24天】Go语言标准库中的`text/template`包用于动态生成HTML和文本,但不熟悉其用法可能导致错误。本文探讨了三个常见问题:1) 忽视模板执行错误,应确保正确处理错误;2) 忽视模板安全,应使用`html/template`包防止XSS攻击;3) 模板结构不合理,应合理组织模板以提高可维护性。理解并运用这些最佳实践,能提升Go语言模板编程的效率和安全性,助力构建稳健的Web应用。
15 0
|
4天前
|
程序员 Go
Golang深入浅出之-Select语句在Go并发编程中的应用
【4月更文挑战第23天】Go语言中的`select`语句是并发编程的关键,用于协调多个通道的读写。它会阻塞直到某个通道操作可行,执行对应的代码块。常见问题包括忘记初始化通道、死锁和忽视`default`分支。要解决这些问题,需确保通道初始化、避免死锁并添加`default`分支以处理无数据可用的情况。理解并妥善处理这些问题能帮助编写更高效、健壮的并发程序。结合使用`context.Context`和定时器等工具,可提升`select`的灵活性和可控性。
17 2
|
1天前
|
JSON JavaScript 前端开发
Golang深入浅出之-Go语言JSON处理:编码与解码实战
【4月更文挑战第26天】本文探讨了Go语言中处理JSON的常见问题及解决策略。通过`json.Marshal`和`json.Unmarshal`进行编码和解码,同时指出结构体标签、时间处理、omitempty使用及数组/切片区别等易错点。建议正确使用结构体标签,自定义处理`time.Time`,明智选择omitempty,并理解数组与切片差异。文中提供基础示例及时间类型处理的实战代码,帮助读者掌握JSON操作。
12 1
Golang深入浅出之-Go语言JSON处理:编码与解码实战
|
1天前
|
安全 Go 开发者
Golang深入浅出之-Go语言模板(text/template):动态生成HTML
【4月更文挑战第25天】Go语言的`text/template`和`html/template`库提供动态HTML生成。本文介绍了模板基础,如基本语法和数据绑定,以及常见问题和易错点,如忘记转义、未初始化变量、复杂逻辑处理和错误处理。建议使用`html/template`防止XSS攻击,初始化数据结构,分离业务逻辑,并严谨处理错误。示例展示了条件判断和循环结构。通过遵循最佳实践,开发者能更安全、高效地生成HTML。
7 0
|
2天前
|
数据管理 Go 开发者
Golang深入浅出之-Go语言上下文(context)包:处理取消与超时
【4月更文挑战第25天】Go语言中的`context`包在并发、网络请求和长任务中至关重要,提供取消、截止时间和元数据管理。本文探讨`context`基础,如`Background()`、`TODO()`、`WithCancel()`、`WithDeadline()`和`WithTimeout()`。常见问题包括不当传递、过度使用`Background()`和`TODO()`以及忽略错误处理。通过取消和超时示例,强调正确传递上下文、处理取消错误和设置超时以提高应用健壮性和响应性。正确使用`context`是构建稳定高效Go应用的关键。
12 1
|
2天前
|
Unix Linux Go
Golang深入浅出之-信号(Signals)处理与优雅退出Go程序
【4月更文挑战第25天】Go语言中的信号处理关乎程序对外部事件的响应,尤其是优雅地终止进程。本文介绍了信号基础,如SIGINT、SIGTERM等常见信号,以及处理流程:注册处理器、等待信号、执行清理和优雅退出。强调了三个易错点及避免方法,并提供实战代码示例展示如何监听和响应信号。信号处理应简洁高效,确保程序健壮性和用户体验。
10 0
|
4天前
|
Go 开发者
Golang深入浅出之-Go语言上下文(context)包:处理取消与超时
【4月更文挑战第23天】Go语言的`context`包提供`Context`接口用于处理任务取消、超时和截止日期。通过传递`Context`对象,开发者能轻松实现复杂控制流。本文解析`context`包特性,讨论常见问题和解决方案,并给出代码示例。关键点包括:1) 确保将`Context`传递给所有相关任务;2) 根据需求选择适当的`Context`创建函数;3) 定期检查`Done()`通道以响应取消请求。正确使用`context`包能提升Go程序的控制流管理效率。
14 1
|
4天前
|
Go
Golang深入浅出之-信号(Signals)处理与优雅退出Go程序
【4月更文挑战第23天】在Go语言中,使用`os/signal`包处理信号对实现程序优雅退出和响应中断至关重要。本文介绍了如何注册信号处理器、处理常见问题和错误,以及提供代码示例。常见问题包括未捕获关键信号、信号处理不当导致程序崩溃和忽略清理逻辑。解决方案包括注册信号处理器(如`SIGINT`、`SIGTERM`)、保持信号处理器简洁和执行清理逻辑。理解并正确应用这些原则能增强Go程序的健壮性和可管理性。
14 1
|
4天前
|
存储 安全 Go
Golang深入浅出之-原子操作包(sync/atomic)在Go中的应用
【4月更文挑战第23天】Go语言的`sync/atomic`包支持原子操作,防止多线程环境中的数据竞争。包括原子整数和指针操作,以及原子标量函数。常见问题包括误用非原子操作、误解原子操作语义和忽略内存排序约束。解决方法是使用原子函数、结合其他同步原语和遵循内存约束。注意始终使用原子操作处理共享变量,理解其语义限制,并熟悉内存排序约束,以实现并发安全和高效的应用程序。
18 1
|
5天前
|
安全 Go 开发者
Golang深入浅出之-Go语言并发编程面试:Goroutine简介与创建
【4月更文挑战第22天】Go语言的Goroutine是其并发模型的核心,是一种轻量级线程,能低成本创建和销毁,支持并发和并行执行。创建Goroutine使用`go`关键字,如`go sayHello("Alice")`。常见问题包括忘记使用`go`关键字、不正确处理通道同步和关闭、以及Goroutine泄漏。解决方法包括确保使用`go`启动函数、在发送完数据后关闭通道、设置Goroutine退出条件。理解并掌握这些能帮助开发者编写高效、安全的并发程序。
13 1