6.程序测试和文档
6.1程序测试
3.样本测试
测试源码文件中还可以添加样本测试函数,但编写样本测试函数不需要使用 testing 代码包的 API。
(1).编写样本测试函数
样本测试函数的名称需要以“Example”作为开始。在这类函数的函数体的最后还可以有若干个注释行。它们作用是比较在该测试函数被执行期间,标准输出上出现的内容是否与预期的相符。但要想这些注释航被正确地解析,需满足下面几个条件:
这些注释行必须出现在函数体的末尾,且在它们和作为当前函数体结束符的“}”之间没有任何代码。否则,注释行对于样本测试来说是无效的。
在第一行注释中,紧跟在单行注释前导符//之后的永远应该是Output:,否则这些注释行对于样本测试来说是无效的。
在Output: 右边的内容以及后续注释行中的内容都分别代表了标准输出中的一行内容。
如下一个测试源码文件 et_test.go,位于工作区 src 下的 testing/et 代码包中:
package et
import (
"fmt"
)
func ExampleHello() {
fmt.Println("Hello, Huazie")
// Output: Hello, Huazie
}
(2).样本测试的运行
使用 go test 命令对上面的源码文件 et_test.go 进行测试,运行截图如下:
调用 fmt.Println 函数所产生的打印内容并不会真正地出现在标准输出上,而只会被用于与样本注释行中的内容做对比。现在修改 fmt.Println 中内容为“Hello, Huazie1”,运行结果截图如下:
现在将样本测试函数 ExampleHello 修改成这样:
func ExampleHello() {
for i := 0; i < 3; i++ {
fmt.Println("Hello, Huazie")
}
// Output: Hello, Huazie
// Hello, Huazie
// Hello, Huazie
}
运行截图如下:
测试仍然通过,不过需要注意,在样本测试函数的函数体末尾的多个样本注释行必须是连续的,在它们之间不能间隔任何行,即使是空行也不行。命令程序只会把在样本测试函数的函数体中的紧挨着当前函数体结束符“}”的注释行视为样本注释行。如果一个样本测试函数中没有任何样本注释行,那么这个函数仅仅会被编译而不会执行。
(3).样本测试函数的命名
样本测试函数命名规则:
当被测试对象是整个代码包时,样本测试函数的名称应该是Example,即直接以样本测试函数名的统一前缀作为函数名称。
当被测试对象是一个函数时,对于函数F,样本测试函数的名称应该是ExampleF。
当被测试对象是一个类型时,对于类型T,样本测试函数的名称应该是ExampleT。
当被测试对象是某个类型中的一个方法时,对于类型T的方法M,样本测试函数的名称应该是ExampleT_M。
如果需要在样本测试函数的名称上添加后缀,那么需要用下划线把该后缀与名称的其他部分分隔开。并且,该后缀的首字母必须小写。例如:针对类型 T 的方法 M 且需要加入后缀“basic”的样本测试函数的名称应该是ExampleT_M_basic。
4.测试运行记录
在硬件环境方面,主要考察计算机的负载状况,比如CPU使用率、内存使用率、磁盘使用情况等。
在软件系统方面,主要包括内存分配、并发处理数量及死锁等情况
(1).收集资源使用情况
与收集资源使用情况有关的标记
标记名称 | 标记描述 |
---|---|
-cpuprofile cpu.out | 记录CPU使用情况,并写到指定的文件中,直到测试退出。 cpu.out作为指定文件的文件名可以被其他任何名称代替 |
-memprofile mem.out | 记录内存使用情况,并在所有测试通过后将内存使用概要写到指定的文件中。 mem.out作为指定文件的文件名可以被其他任何名称代替 |
-memprofilerate n | 此标记控制着记录内存分配操作的行为,这些记录将会被写到内存使用概要文件中。 N代表着分析器的取样间隔,单位为字节。每当有n个字节的内存被分配时,分析器就会取样一次 |
现在执行 -cpuprofile 标记的 go test 命令来运行标准库的 net 代码包中的测试。例如:
E:\Software\Go\goc2p\pprof>go test -cpuprofile cpu.out net
go test 命令程序不允许在多个代码包测试的时候使用-cpuprofile标记,如下:
在上面的测试运行完成后,在当前目录中生成了一个 net.test.exe 的可执行文件和 cpu.out 的文件,可以使用 go tool pprof 命令来交互式的对这个概要文件进行查阅,如下:
当 -memprofile 标记有效时,测试运行程序会在运行测试的同时记录它们对内存的使用情况(程序运行期间的堆内存的分配情况)。
使用 -memprofilerate 标记来设置分析器的取样间隔,单位是字节。它的值越小就意味着取样效果越好,因为取样间隔会更短。在 testing 包内部,-memprofilerate 标记的值会赋给 runtime 包中的 int 类型的变量 MemProfileRate,该变量默认值为 512 * 1024,即 512K 字节。如果设置为 0,则代表停止取样。
-memprofile 标记和 -memprofilerate 标记的测试命令语句,如下:
E:\Software\Go\goc2p\pprof>go test -memprofile mem.out -memprofilerate 10 net
执行上面命令之后,在当前目录中生成了一个 net.test.exe 的可执行文件和 cpu.out 的文件,可以使用 go tool pprof 命令来交互式的对这个概要文件进行查阅,如下:
(2).记录程序阻塞事件
与记录程序阻塞事件有关的标记
标记名称 | 标记描述 |
---|---|
-blockprofile block.out | 记录Goroutine阻塞事件,并在所有测试通过后将概要信息写到指定的文件中。 block.out作为指定文件的文件名可以被其他任何名称代替 |
-blockprofilerate n | 该标记用于控制记录Goroutine阻塞事件的时间间隔,n的单位为次,默认值为1 |
-blockprofile 和 -blockprofilerate 标记的测试命令语句,如下:
E:\Software\Go\goc2p\pprof>go test -blockprofile block.out -blockprofilerate 100 net
-blockprofilerate 标记的值也是通过标准库代码包 runtime 中的 API—-函数 SetBlockProfileRate —-传递给Go运行时系统的。当传入的参数为 0 时,就相当于彻底取消记录操作。当传入的参数为 1 时,每个阻塞事件都将被记录。-blockprofilerate 标记的默认值为 1。
5.测试覆盖率
测试覆盖率是指,作为被测试对象的代码包中的代码有多少在刚刚执行的测试中被使用到。如果执行的该测试致使当前代码包中的90%的语句都被执行了,那么该测试的测试覆盖率就是90%。
go test 命令可接受的与测试覆盖率有关的标记
标记名称 | 使用示例 | 说明 |
---|---|---|
-cover | -cover | 启用测试覆盖率分析 |
-covermode | -covermode=set | 自动添加-cover标记并设置不同的测试覆盖率统计模式,支持的模式共有以下3个。 set:只记录语句是否被执行过 count: 记录语句被执行的次数 atomic: 记录语句被执行的次数,并保证在并发执行时也能正确计数,但性能会受到一定的影响 这几个模式不可以被同时使用,在默认情况下,测试覆盖率的统计模式是set |
-coverpkg | -coverpkg bufio,net | 自动添加-cover标记并对该标记后罗列的代码包中的程序进行测试覆盖率统计。 在默认情况下,测试运行程序会只对被直接测试的代码包中的程序进行统计。 该标记意味着在测试中被间接使用到的其他代码包中的程序也可以被统计。 另外,代码包需要由它的导入路径指定,且多个导入路径之间以逗号“,”分隔。 |
-coverprofile | -coverprofile cover.out | 自动添加-cover标记并把所有通过的测试的覆盖率的概要写入指定的文件中 |
在 go test 命令后加入 -cover 标记以开启测试覆盖率统计。如下:
标记 -coverpkg 可以获得间接被使用的代码包中的程序在测试期间的执行率。如下:
在使用 -coverpkg 标记来指定要被统计的代码包之后,未被指定的代码则肯定不会被统计,即使是被直接测试的那个代码包。
标记 -coverprofile 会使测试运行程序把测试覆盖率的统计信息写入到指定的文件中。该文件会被放在执行 go test 命令时的那个目录下。如下:
对于 cover.out 的内容可以使用Go语言提供的 cover 工具查看,通过 go tool cover 命令来运行它。cover 工具的两个功能:
根据指定的规则重写某一个源码文件中的代码,并输出指定的目标上。
读取测试覆盖率的统计信息文件,并以指定的方式呈现。
对于第一个功能,运行附带了测试覆盖率相关标记的go test命令之后,测试运行程序会使用 cover 工具在被测试的代码包中的非测试源码文件被编译之前重写它们。重写的方式会由运行 go test 命令时的 -covermode 标记所指定的测试覆盖率统计模式决定。在默认情况下,这个统计模式为 set 。
set统计模式
在 testing/ct 下,有如下的源码文件 ct_demo.go,如下:
package ct
func TypeCategoryOf(v interface{}) string {
switch v.(type) {
case bool:
return "boolean"
case int, uint, int8, uint8, int16, uint16, int32, uint32, int64, uint64:
return "integer"
case float32, float64:
return "float"
case complex64, complex128:
return "complex"
case string:
return "string"
}
return "unknown"//如果只是非基本类型,统一返回“unknown”
}
在运行 go test –cover 命令之后,测试运行程序 cover 工具会把 TypeCategoryOf 函数重写成如下:
func TypeCategoryOf(v interface{}) string {
GoCover.Count[0] = 1
switch v.(type) {
case bool:
GoCover.Count[2] = 1
return "boolean"
case int, uint, int8, uint8, int16, uint16, int32, uint32, int64, uint64:
GoCover.Count[3] = 1
return "integer"
case float32, float64:
GoCover.Count[4] = 1
return "float"
case complex64, complex128:
GoCover.Count[5] = 1
return "complex"
case string:
GoCover.Count[6] = 1
return "string"
}
GoCover.Count[1] = 1
return "unknown"
}
原先的源代码中的每一个流程分支上都会被安插一个计数器(由 GoCover.Count 代表)。每当某个流程分支被执行的时候,相应的计数器就会被赋值为 1,这样的方式只能表示该分支是否被执行过。
变量 GoCover 代表一个匿名的结构体类型的值,它的值用于存储每个分支的计数值、每个分支的起始和结束位置在当前源码文件中的行数以及每个分支中的语句的条数等信息。变量 GoCover 暂且可以称为执行计数变量,它的名称是可以通过 -var 标记告知给 cover 工具。
go test 命令的测试目标可以是一个或多个代码包,而 cover 工具每次只能重写一个源码文件。如果被测试的源码文件有多个,那么 go test 命令会对每一个文件都运行一次 cover 工具。为了使被重写的每一个源码文件中的执行计数变量的名称不重复,go test 命令针对不同的源码文件传递给 cover 工具的 -var 标记的值也是不同的。
count统计模式
在set统计模式下的如下语句:
GoCover.Count[0] = 1
会被写为
GoCover.Count[0]++
这就相当于对分支的每一次执行都进行了记录。
atomic统计模式
set统计模式的语句会被写成如下:
_cover_atomic_.AddUint32(&CoverVar.Count[1], 1)
其中 cover_atomic 是代码包 sync/atomic 的别名,需要在源代码中导入如下语句:
import _cover_atomic_ "sync/atomic"
atomic 统计模式一般只在存在并发执行的应用场景下才被使用,因为原子操作的执行会带来一定的性能损耗。而 set 和 count 统计模式则比它通用的多,它们对于原先的程序的执行成本的影响也小很多。
通过 -mode 标记把需要使用的测试覆盖率统计模式直接传递给 cover 工具。它的用法和含义与 go test 命令的 -covermode 标记一致。实际上,go test 命令会把 -covermode 标记的值原封不动地作为运行 cover 工具是传递给它的 -mode 标记的值。但是,-mode 标记并没有默认值,因此使用 cover 工具对某个源代码文件进行重写的时候必须添加 -mode 标记。
在默认情况下,被重写的源代码会输出到标准输出上。但是也可以使用 -o 标记把这些代码存放到指定的文件中。
如下使用 cover 工具对工作区 src 目录下的 testing\ct 代码包下的 ct_demo.go 进行重写:
E:\Software\Go\goc2p\src\testing\ct>go tool cover -mode=set -var=”GoCover” -o ct _out.go ct_demo.go
这时 ct_out.go 就是 ct_demo.go 被重写后的源码文件,各位可以自行运行查看。
对于 cover 工具的第二个功能:
查看之前 go test cnet/ctcp -coverprofile=cover.out 命令为 cnet/ctcp 代码包生成了一个测试覆盖率的概要文件 cover.out,如下运行截图:
标记 -func 可以让 cover 工具把概要文件中包含的每个函数的测试覆盖率概要信息打印到标准输出上。
在上面的这段输出内容中,除了最后一行,每一行的内容都包括了3项信息,分别是:函数所在的源码文件的相对路径、函数名称和函数中被测试到的语句的数量占该函数的语句总数的百分比。而最后一行内容中的百分比则是被测试代码包中的所有测试到的语句的数量占该代码包中的语句总数的百分比。
现在使用 -html 标记,可以看到更加图形化的信息来直观的反应统计情况。如下:
go tool cover -html=cover.out
该命令会立即返回并且在标准输出上也并不会出现任何内容。取而代之的是当前操作系统的默认浏览器会被启动并显示 cover 工具刚刚根据概要文件生成的 HTML 格式的页面文件。如下:
在这个 HTML 页面中,被测试到的语句以绿色显示,未被测试到的语句以红色显示,而未参加测试覆盖率计算的语句则用灰色表示。左上角还可以通过下拉框选择被测试代码包中的不同源码文件以查看它们的测试覆盖率情况。把鼠标的光标停留在绿色的语句上面的时候,在光标附近还会出现该语句执行次数的数字,各位可以自己试试。
上面的 HTML 页面文件展示的是在 set 统计模式下生成的概要文件。在 count 和 atomic 统计模式下,各位可以自己试试,参照如下:
cover工具可接受的标记
标记名称 | 使用示例 | 说明 |
---|---|---|
-func | -func=cover.out | 根据概要文件(即cover.out)中的内容,输出每一个被测试函数的测试覆盖率概要信息 |
-html | -html=cover.out | 把概要文件中的内容转换成HTML格式的文件,并使用当前操作系统中的默认网络浏览器查看它 |
-mode | -mode=count | 被用于设置测试概要文件的统计模式,详见go test命令的-covermode标记 |
-o | -o=cover.out | 把重写后的源代码输出到指定文件中,如果不添加此标记,那么重写后的源代码会输出到标准输出上 |
-var | -var=GoCover | 设置被添加到原先的源代码中的额外变量的名称 |
6.2程序文档
在Go语言中,可以使用 godoc 命令在本机启动一个可被用于查看本机所有工作区中的所有代码包文档的 Web 服务。命令行中输入如下命令并执行:
godoc -http=:9090 –index
在浏览器中就可以像如下输入地址查看:
1.编写程序注释
Go语言在注释风格中融入了 C 语言和 C++ 语言的特点。既可以使用 C++ 语言风格的行注释:
// 行注释
又可以使用 C 语言风格的块注释:
/*
块注释
*/
注意:无论是哪一种风格的注释都不能出现嵌套的情况。
2.代码包的注释
每个代码包都应包含一段独立的代码包注释。这段注释应该对当前代码包的功能和用途进行总体性的介绍。按照惯例,代码包注释应该被存放在当前代码包目录下的 doc.go 文件中。在这个 doc.go 文件中,应该有与包中其他源码文件相同的代码包声明语句,并在该声明语句之上以块注释的方式插入代码包注释。
当代码包中仅有一个源码文件的时候,建议源码文件的主文件名与当前代码包的名称相同。当然,如果在代码包中只有一个源码文件或代码包注释比较短,也可以将注释插入到当前代码包同名的源码文件中的代码包声明语句之上。
在标准库中,代码包 fmt 的代码包注释被放在了 $GOROOT/src/fmt/doc.go 中,与其相对应的文档页面如下:
在 Overview 中出现的就是 fmt 包的代码包注释行,也就是定义在 fmt/doc.go 中的。
3.程序实体的注释
在上图中的(带有链接的)Index 栏包含的是当前代码中的所有课导出的程序实体的文档的索引。点击后显示如下信息:
点击其中一个索引 type State 之后页面定位到与该索引相对应的程序实体文档的锚点处。例如:
点击有下划线的 State 处并点击,页面跳转到声明 fmt.State 的源代码处,如下:
程序实体的文档即是它的声明代码以及紧挨在上面的行注释,这里也体现了“代码即注释”的思想。
4.常量和变量的注释
Go语言语法允许一次声明一组常量或变量,程序实体声明编写注释如下示例:
// IP address lengths (bytes).
const (
IPv4len = 4
IPv6len = 16
)
上面代码位于代码包 net 中的源码文件 ip.go。对于这样一组声明来说,对它们的描述统一放在关键字 const(或关键字 var)之上且紧邻的注释行中,而不是分别为每一个常量(或变量)声明添加注释。
本篇讲解了Go语言程序测试的样本测试、测试运行记录、测试覆盖率和程序文档的知识。
最后附上国内的Go语言社区(每篇更新一个)
Go友团:该社区致力于为Go语言爱好者提供学习与交流的最佳环境。社区的组织者们都非常热爱Go语言,乐于分享,其技术精湛。其中包括了 Beego 的创始人。社区主页地址为: http://golanghome.com
有疑问加站长微信联系(非本文作者)