Go语言核心36讲(Go语言实战与应用二)--学习笔记
24 | 测试的基本规则和流程(下)
Go 语言是一门很重视程序测试的编程语言,所以在上一篇中,我与你再三强调了程序测试的重要性,同时,也介绍了关于go test命令的基本规则和主要流程的内容。今天我们继续分享测试的基本规则和流程。
知识扩展
问题 1:怎样解释功能测试的测试结果?
demo53.go
package main
import (
"errors"
"flag"
"fmt"
)
var name string
func init() {
flag.StringVar(&name, "name", "everyone", "The greeting object.")
}
func main() {
flag.Parse()
greeting, err := hello(name)
if err != nil {
fmt.Printf("error: %s\n", err)
return
}
fmt.Println(greeting, introduce())
}
// hello 用于生成问候内容。
func hello(name string) (string, error) {
if name == "" {
return "", errors.New("empty name")
}
return fmt.Sprintf("Hello, %s!", name), nil
}
// introduce 用于生成介绍内容。
func introduce() string {
return "Welcome to my Golang column."
}
demo53_test.go
package main
import (
"fmt"
"testing"
)
func TestHello(t *testing.T) {
var name string
greeting, err := hello(name)
if err == nil {
t.Errorf("The error is nil, but it should not be. (name=%q)",
name)
}
if greeting != "" {
t.Errorf("Nonempty greeting, but it should not be. (name=%q)",
name)
}
name = "Robert"
greeting, err = hello(name)
if err != nil {
t.Errorf("The error is not nil, but it should be. (name=%q)",
name)
}
if greeting == "" {
t.Errorf("Empty greeting, but it should not be. (name=%q)",
name)
}
expected := fmt.Sprintf("Hello, %s!", name)
if greeting != expected {
t.Errorf("The actual greeting %q is not the expected. (name=%q)",
greeting, name)
}
t.Logf("The expected greeting is %q.\n", expected)
}
func TestIntroduce(t *testing.T) {
intro := introduce()
expected := "Welcome to my Golang column."
if intro != expected {
t.Errorf("The actual introduce %q is not the expected.",
intro)
}
t.Logf("The expected introduce is %q.\n", expected)
}
func TestFail(t *testing.T) {
//t.Fail()
t.FailNow() // 此调用会让当前的测试立即失败。
t.Log("Failed.")
}
我们先来看下面的测试命令和结果:
$ go test puzzlers/article20/q2
ok puzzlers/article20/q2 0.008s
以$符号开头表明此行展现的是我输入的命令。在这里,我输入了go test puzzlers/article20/q2,这表示我想对导入路径为puzzlers/article20/q2的代码包进行测试。代码下面一行就是此次测试的简要结果。
这个简要结果有三块内容。最左边的ok表示此次测试成功,也就是说没有发现测试结果不如预期的情况。
当然了,这里全由我们编写的测试代码决定,我们总是认定测试代码本身没有 Bug,并且忠诚地落实了我们的测试意图。在测试结果的中间,显示的是被测代码包的导入路径。
而在最右边,展现的是此次对该代码包的测试所耗费的时间,这里显示的0.008s,即 8 毫秒。不过,当我们紧接着第二次运行这个命令的时候,输出的测试结果会略有不同,如下所示:
$ go test puzzlers/article20/q2
ok puzzlers/article20/q2 (cached)
可以看到,结果最右边的不再是测试耗时,而是(cached)。这表明,由于测试代码与被测代码都没有任何变动,所以go test命令直接把之前缓存测试成功的结果打印出来了。
go 命令通常会缓存程序构建的结果,以便在将来的构建中重用。我们可以通过运行go env GOCACHE命令来查看缓存目录的路径。缓存的数据总是能够正确地反映出当时的各种源码文件、构建环境、编译器选项等等的真实情况。
一旦有任何变动,缓存数据就会失效,go 命令就会再次真正地执行操作。所以我们并不用担心打印出的缓存数据不是实时的结果。go 命令会定期地删除最近未使用的缓存数据,但是,如果你想手动删除所有的缓存数据,运行一下go clean -cache命令就好了。
对于测试成功的结果,go 命令也是会缓存的。运行go clean -testcache将会删除所有的测试结果缓存。不过,这样做肯定不会删除任何构建结果缓存。
此外,设置环境变量GODEBUG的值也可以稍稍地改变 go 命令的缓存行为。比如,设置值为gocacheverify=1将会导致 go 命令绕过任何的缓存数据,而真正地执行操作并重新生成所有结果,然后再去检查新的结果与现有的缓存数据是否一致。
总之,我们并不用在意缓存数据的存在,因为它们肯定不会妨碍go test命令打印正确的测试结果。
你可能会问,如果测试失败,命令打印的结果将会是怎样的?如果功能测试函数的那个唯一参数被命名为t,那么当我们在其中调用t.Fail方法时,虽然当前的测试函数会继续执行下去,但是结果会显示该测试失败。如下所示:
$ go test puzzlers/article20/q2
--- FAIL: TestFail (0.00s)
demo53_test.go:49: Failed.
FAIL
FAIL puzzlers/article20/q2 0.007s
我们运行的命令与之前是相同的,但是我新增了一个功能测试函数TestFail,并在其中调用了t.Fail方法。测试结果显示,对被测代码包的测试,由于TestFail函数的测试失败而宣告失败。
func TestFail(t *testing.T) {
t.Fail()
t.Log("Failed.")
}
注意,对于失败测试的结果,go test命令并不会进行缓存,所以,这种情况下的每次测试都会产生全新的结果。另外,如果测试失败了,那么go test命令将会导致:失败的测试函数中的常规测试日志一并被打印出来。
在这里的测试结果中,之所以显示了“demo53_test.go:49: Failed.”这一行,是因为我在TestFail函数中的调用表达式t.Fail()的下边编写了代码t.Log("Failed.")。
t.Log方法以及t.Logf方法的作用,就是打印常规的测试日志,只不过当测试成功的时候,go test命令就不会打印这类日志了。如果你想在测试结果中看到所有的常规测试日志,那么可以在运行go test命令的时候加入标记-v。
若我们想让某个测试函数在执行的过程中立即失败,则可以在该函数中调用t.FailNow方法。
我在下面把TestFail函数中的t.Fail()改为t.FailNow()。
func TestFail(t *testing.T) {
//t.Fail()
t.FailNow() // 此调用会让当前的测试立即失败。
t.Log("Failed.")
}
与t.Fail()不同,在t.FailNow()执行之后,当前函数会立即终止执行。换句话说,该行代码之后的所有代码都会失去执行机会。
在这样修改之后,我再次运行上面的命令,得到的结果如下:
--- FAIL: TestFail (0.00s)
FAIL
FAIL puzzlers/article20/q2 0.008s
显然,之前显示在结果中的常规测试日志并没有出现在这里。
顺便说一下,如果你想在测试失败的同时打印失败测试日志,那么可以直接调用t.Error方法或者t.Errorf方法。
前者相当于t.Log方法和t.Fail方法的连续调用,而后者也与之类似,只不过它相当于先调用了t.Logf方法。
除此之外,还有t.Fatal方法和t.Fatalf方法,它们的作用是在打印失败错误日志之后立即终止当前测试函数的执行并宣告测试失败。更具体地说,这相当于它们在最后都调用了t.FailNow方法。
好了,到此为止,你是不是已经会解读功能测试的测试结果了呢?
问题 2:怎样解释性能测试的测试结果?
性能测试与功能测试的结果格式有很多相似的地方。我们在这里仅关注前者的特殊之处。请看下面的打印结果。
$ go test -bench=. -run=^$ puzzlers/article20/q3
goos: darwin
goarch: amd64
pkg: puzzlers/article20/q3
BenchmarkGetPrimes-8 500000 2314 ns/op
PASS
ok puzzlers/article20/q3 1.192s
我在运行go test命令的时候加了两个标记。第一个标记及其值为-bench=.,只有有了这个标记,命令才会进行性能测试。该标记的值.表明需要执行任意名称的性能测试函数,当然了,函数名称还是要符合 Go 程序测试的基本规则的。
第二个标记及其值是-run=$,这个标记用于表明需要执行哪些功能测试函数,这同样也是以函数名称为依据的。该标记的值$意味着:只执行名称为空的功能测试函数,换句话说,不执行任何功能测试函数。
你可能已经看出来了,这两个标记的值都是正则表达式。实际上,它们只能以正则表达式为值。此外,如果运行go test命令的时候不加-run标记,那么就会使它执行被测代码包中的所有功能测试函数。
再来看测试结果,重点说一下倒数第三行的内容。BenchmarkGetPrimes-8被称为单个性能测试的名称,它表示命令执行了性能测试函数BenchmarkGetPrimes,并且当时所用的最大 P 数量为8。
最大 P 数量相当于可以同时运行 goroutine 的逻辑 CPU 的最大个数。这里的逻辑 CPU,也可以被称为 CPU 核心,但它并不等同于计算机中真正的 CPU 核心,只是 Go 语言运行时系统内部的一个概念,代表着它同时运行 goroutine 的能力。
顺便说一句,一台计算机的 CPU 核心的个数,意味着它能在同一时刻执行多少条程序指令,代表着它并行处理程序指令的能力。
我们可以通过调用 runtime.GOMAXPROCS函数改变最大 P 数量,也可以在运行go test命令时,加入标记-cpu来设置一个最大 P 数量的列表,以供命令在多次测试时使用。
至于怎样使用这个标记,以及go test命令执行的测试流程,会因此做出怎样的改变,我们在下一篇文章中再讨论。
在性能测试名称右边的是,go test命令最后一次执行性能测试函数(即BenchmarkGetPrimes函数)的时候,被测函数(即GetPrimes函数)被执行的实际次数。这是什么意思呢?
go test命令在执行性能测试函数的时候会给它一个正整数,若该测试函数的唯一参数的名称为b,则该正整数就由b.N代表。我们应该在测试函数中配合着编写代码,比如:
for i := 0; i < b.N; i++ {
GetPrimes(1000)
}
我在一个会迭代b.N次的循环中调用了GetPrimes函数,并给予它参数值1000。go test命令会先尝试把b.N设置为1,然后执行测试函数。
如果测试函数的执行时间没有超过上限,此上限默认为 1 秒,那么命令就会改大b.N的值,然后再次执行测试函数,如此往复,直到这个时间大于或等于上限为止。
当某次执行的时间大于或等于上限时,我们就说这是命令此次对该测试函数的最后一次执行。这时的b.N的值就会被包含在测试结果中,也就是上述测试结果中的500000。
我们可以简称该值为执行次数,但要注意,它指的是被测函数的执行次数,而不是性能测试函数的执行次数。
最后再看这个执行次数的右边,2314 ns/op表明单次执行GetPrimes函数的平均耗时为2314纳秒。这其实就是通过将最后一次执行测试函数时的执行时间,除以(被测函数的)执行次数而得出的。
(性能测试结果的基本解读)
以上这些,就是对默认情况下的性能测试结果的基本解读。你看明白了吗?
demo54.go
package q3
import (
"math"
)
// GetPrimes 用于获取小于或等于参数max的所有质数。
// 本函数使用的是爱拉托逊斯筛选法(Sieve Of Eratosthenes)。
func GetPrimes(max int) []int {
if max <= 1 {
return []int{}
}
marks := make([]bool, max)
var count int
squareRoot := int(math.Sqrt(float64(max)))
for i := 2; i <= squareRoot; i++ {
if marks[i] == false {
for j := i * i; j < max; j += i {
if marks[j] == false {
marks[j] = true
count++
}
}
}
}
primes := make([]int, 0, max-count)
for i := 2; i < max; i++ {
if marks[i] == false {
primes = append(primes, i)
}
}
return primes
}
demo54_test.go
package q3
import "testing"
func BenchmarkGetPrimes(b *testing.B) {
for i := 0; i < b.N; i++ {
GetPrimes(1000)
}
}
总结
注意,对于功能测试和性能测试,命令执行测试流程的方式会有些不同。另外一个重要的问题是,我们在与go test命令交互时,怎样解读它提供给我们的信息。只有解读正确,你才能知道测试的成功与否,失败的具体原因以及严重程度等等。
除此之外,对于性能测试,你还需要关注命令输出的计算资源使用提示,以及各种性能度量。
这两篇的文章中,我们一起学习了不少东西,但是其实还不够。我们只是探讨了go test命令以及testing包的基本使用方式。
在下一篇,我们还会讨论更高级的内容。这将涉及go test命令的各种标记、testing包的更多 API,以及更复杂的测试结果。
思考题
在编写示例测试函数的时候,我们怎样指定预期的打印内容?
笔记源码
https://github.com/MingsonZheng/go-core-demo
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。
Go语言核心36讲(Go语言实战与应用二)--学习笔记的更多相关文章
- Go语言核心36讲(Go语言基础知识三)--学习笔记
03 | 库源码文件 在我的定义中,库源码文件是不能被直接运行的源码文件,它仅用于存放程序实体,这些程序实体可以被其他代码使用(只要遵从 Go 语言规范的话). 这里的"其他代码" ...
- Go语言核心36讲(Go语言实战与应用一)--学习笔记
23 | 测试的基本规则和流程 (上) 在接下来的日子里,我将带你去学习在 Go 语言编程进阶的道路上,必须掌握的附加知识,比如:Go 程序测试.程序监测,以及 Go 语言标准库中各种常用代码包的正确 ...
- Go语言核心36讲(Go语言实战与应用三)--学习笔记
25 | 更多的测试手法 在本篇文章,我会继续为你讲解更多更高级的测试方法.这会涉及testing包中更多的 API.go test命令支持的,更多标记更加复杂的测试结果,以及测试覆盖度分析等等. 前 ...
- Go语言核心36讲(Go语言实战与应用四)--学习笔记
26 | sync.Mutex与sync.RWMutex 从本篇文章开始,我们将一起探讨 Go 语言自带标准库中一些比较核心的代码包.这会涉及这些代码包的标准用法.使用禁忌.背后原理以及周边的知识. ...
- Go语言核心36讲(Go语言实战与应用十二)--学习笔记
34 | 并发安全字典sync.Map (上) 我们今天再来讲一个并发安全的高级数据结构:sync.Map.众所周知,Go 语言自带的字典类型map并不是并发安全的. 前导知识:并发安全字典诞生史 换 ...
- Go语言核心36讲(Go语言实战与应用十四)--学习笔记
36 | unicode与字符编码 在开始今天的内容之前,我先来做一个简单的总结. Go 语言经典知识总结 在数据类型方面有: 基于底层数组的切片: 用来传递数据的通道: 作为一等类型的函数: 可实现 ...
- Go语言核心36讲(Go语言实战与应用十八)--学习笔记
40 | io包中的接口和工具 (上) 我们在前几篇文章中,主要讨论了strings.Builder.strings.Reader和bytes.Buffer这三个数据类型. 知识回顾 还记得吗?当时我 ...
- Go语言核心36讲(Go语言实战与应用二十二)--学习笔记
44 | 使用os包中的API (上) 我们今天要讲的是os代码包中的 API.这个代码包可以让我们拥有操控计算机操作系统的能力. 前导内容:os 包中的 API 这个代码包提供的都是平台不相关的 A ...
- Go语言核心36讲(Go语言实战与应用二十四)--学习笔记
46 | 访问网络服务 前导内容:socket 与 IPC 人们常常会使用 Go 语言去编写网络程序(当然了,这方面也是 Go 语言最为擅长的事情).说到网络编程,我们就不得不提及 socket. s ...
随机推荐
- iGuard6.0 — 各适其用的网站防护体系
随着互联网新技术的涌现,网站的架构技术和涉及的资源也日益多样且复杂化.这对网站各类资源的防护工作也提出了更高的挑战和更细粒度的需求. 我们经常碰到的用户真实需求包括: 我的 CMS 制作系统,会不会 ...
- Nresource服务之接口缓存化
1. 背景 Nresource服务日均4.5亿流量,考虑到未来流量急增场景,我们打算对大流量接口进行缓存化处理:根据服务管理平台数据统计显示getUsableResoureCount接口调用量很大,接 ...
- 前端规范之CSS规范(Stylelint)
代码规范是软件开发领域经久不衰的话题,几乎所有工程师在开发过程中都会遇到或思考过这一问题.而随着前端应用的大型化和复杂化,越来越多的前端团队也开始重视代码规范.同样,前段时间,笔者所在的团队也开展了一 ...
- PyTorch固定参数
In situation of finetuning, parameters in backbone network need to be frozen. To achieve this target ...
- SDOI2015 排序
SDOI2015 排序 今天看到这道题,没有一点思路,暴力都没的打...还是理解错题意了,操作不同位置不是说改不同的区间,而是不同操作的顺序...考场上如果知道这个的话最少暴力拿一半啊,因为正解本来就 ...
- pdb的插拔测试
pdb的插拔测试:将pdb从一个cdb中,插拔到另一个cdb中. 源端pdb unplug SQL> select instance_name from v$instance; INSTANCE ...
- 左手IRR,右手NPV,掌握发家致富道路密码
智能手机的普及让世界成为了我们指尖下的方寸之地. 在各种信息爆炸出现的同时,五花八门的理财信息与我们的生活越贴越近.投资不再仅仅是企业行为,对于个人而言,也是很值得关注的内容. 但是落脚到很小的例子之 ...
- gRPC,爆赞
原文链接: gRPC,爆赞 gRPC 这项技术真是太棒了,接口约束严格,性能还高,在 k8s 和很多微服务框架中都有应用. 作为一名程序员,学就对了. 之前用 Python 写过一些 gRPC 服务, ...
- cmd下载慢
是网络的原因,加一个镜像服务器 pip install *** -i https://pypi.tuna.tsinghua.edu.cn/simple
- 自定义ConditionalOnXX注解
一.Conditional注解介绍 对SpringBoot有足够了解的小伙伴应该都用过Conditional系列注解,该注解可用在类或者方法上用于控制Bean的初始化. 常用的Conditional注 ...