极客时间已完结课程限时免费阅读

24 | 测试的基本规则和流程(下)

24 | 测试的基本规则和流程(下)-极客时间

24 | 测试的基本规则和流程(下)

讲述:黄洲君

时长10:35大小6.05M

你好,我是郝林。今天我分享的主题是测试的基本规则和流程的(下)篇。
Go 语言是一门很重视程序测试的编程语言,所以在上一篇中,我与你再三强调了程序测试的重要性,同时,也介绍了关于go test命令的基本规则和主要流程的内容。今天我们继续分享测试的基本规则和流程。本篇代码和指令较多,你可以点击文章查看原文。

知识扩展

问题 1:怎样解释功能测试的测试结果?

我们先来看下面的测试命令和结果:
$ 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函数的测试失败而宣告失败。
注意,对于失败测试的结果,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()
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函数,并给予它参数值1000go test命令会先尝试把b.N设置为1,然后执行测试函数。
如果测试函数的执行时间没有超过上限,此上限默认为 1 秒,那么命令就会改大b.N的值,然后再次执行测试函数,如此往复,直到这个时间大于或等于上限为止。
当某次执行的时间大于或等于上限时,我们就说这是命令此次对该测试函数的最后一次执行。这时的b.N的值就会被包含在测试结果中,也就是上述测试结果中的500000
我们可以简称该值为执行次数,但要注意,它指的是被测函数的执行次数,而不是性能测试函数的执行次数。
最后再看这个执行次数的右边,2314 ns/op表明单次执行GetPrimes函数的平均耗时为2314纳秒。这其实就是通过将最后一次执行测试函数时的执行时间,除以(被测函数的)执行次数而得出的。
(性能测试结果的基本解读)
以上这些,就是对默认情况下的性能测试结果的基本解读。你看明白了吗?

总结

注意,对于功能测试和性能测试,命令执行测试流程的方式会有些不同。另外一个重要的问题是,我们在与go test命令交互时,怎样解读它提供给我们的信息。只有解读正确,你才能知道测试的成功与否,失败的具体原因以及严重程度等等。
除此之外,对于性能测试,你还需要关注命令输出的计算资源使用提示,以及各种性能度量。
这两篇的文章中,我们一起学习了不少东西,但是其实还不够。我们只是探讨了go test命令以及testing包的基本使用方式。
在下一篇,我们还会讨论更高级的内容。这将涉及go test命令的各种标记、testing包的更多 API,以及更复杂的测试结果。

思考题

在编写示例测试函数的时候,我们怎样指定预期的打印内容?
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 12

提建议

上一篇
23 | 测试的基本规则和流程 (上)
下一篇
25 | 更多的测试手法
unpreview
 写留言

精选留言(20)

  • HF
    2020-05-10
    老师,go如何写出可测试的代码,有什么规范吗?

    作者回复: 网上有不少相关的文章。我说3点基本的吧: 1. 隐藏不该暴露的。测试代码相当于使用你程序的一方,不要让它能够影响甚至破坏你的程序。 2. 暴露出来的API应该对使用者友好。换句话会所,如果在写测试代码的时候,你发现API调用起来不那么顺畅,那么就需要考虑程序的API是否得当了。 3. 保持API在风格上的一致性。风格一致的程序有助于使用方快速运用和理解程序。 这3点是基本的原则了,其他的细节上的东西说到底都是为了做到这3点的。测试代码就是对生产代码的前期验证。

    16
  • Louis
    2018-11-23
    老师讲的很棒! 终于补到这里了! 很多东西都是从编程语言本质的角度去解析。很棒!
    11
  • willmeng
    2019-04-14
    请问,如何修改默认测试时间上限呢?

    作者回复: 我不太清楚你说的“默认测试时间”是指什么。 如果是性能测试的话,可以在执行 go test 命令时追加 -benchtime 标记。这实际上是下一篇文章的思考题。 另外,-timeout 标记可用于设定当次测试运行的总时长,一旦超过这个时长就panic。 具体用法可以参考:https://golang.org/cmd/go/#hdr-Testing_flags 。

    7
  • 象牙塔下的渣渣
    2018-10-11
    老师,能不能把每节课后面的思考题给出答案啊?另外,你这个专栏上的内容在您的那本《Go并发编程》上有嘛?

    作者回复: 这个专栏是完全重写的,几乎跟书上的东西没有重叠。答案的话我赶完稿子后面再补吧。现在补精力跟不上。

    6
  • 给力
    2020-03-24
    有个疑问: go test 的相关参数 可以用go help test去看。 但是这里面并没有其他参数比如(-cpu 是最大用几核 -bench 是基准测试参数 -run功能测试参数),有没有help命令找出对应每个参数的含义,我用的时候只需要help一下就能找到对应的参数使用。

    作者回复: 你敲 go help test 并回车之后会发现最后有两段话: The test binary also accepts flags that control execution of the test; these flags are also accessible by 'go test'. See 'go help testflag' for details. For more about build flags, see 'go help build'. For more about specifying packages, see 'go help packages'. 第一段的意思是,一些更详细的参数说明需要敲 go help testflag 查看。第二段的意思是,go test 也支持 go build 的一些参数,那些参数需要敲 go help build 查看,另外怎么指定代码包需要敲 go help packages 查看说明。

    共 2 条评论
    5
  • 左氧佛沙星人
    2019-04-13
    我用编辑器运行的时候,报错: # command-line-arguments [command-line-arguments.test] ./demo52_test.go:10:19: undefined: hello ./demo52_test.go:20:18: undefined: hello ./demo52_test.go:38:11: undefined: introduce 老师能帮忙解答一下吗?
    展开

    作者回复: 初学的时候最好用命令行运行。 两种解决方案: 1. 在当前代码包下运行 go test 命令。这样可以执行当前代码包下的所有功能测试函数。 2. 在当前代码包下运行 go test demo52_test.go demo52.go 命令。这样只会执行 demo52_test.go 文件中的功能测试函数。

    共 3 条评论
    4
  • 疯琴
    2020-01-15
    结果讲解很清楚👍 cpu那里还不明白,我的笔记本2核超线程,显示-4,我设置-cpu参数为20、100、1000都能跑,也更快,可是这有什么意义呢?通常是不是不设置这个参数就好?

    作者回复: 肯定是都能跑的。这个参数主要是为了模拟多核CPU执行程序。这样可以测试并发程序在各种计算环境中的执行情况,包括功能正确性和性能。

    3
  • 海盗船长
    2020-01-15
    go1.12.5 使用go test,多次测试相同的成功测试代码,并没有cached

    作者回复: 我的测试结果如下: q2 git:(master) ✗ go test puzzlers/article20/q2 ok puzzlers/article20/q2 1.369s ➜ q2 git:(master) ✗ go test puzzlers/article20/q2 ok puzzlers/article20/q2 (cached) ➜ q2 git:(master) ✗ go test puzzlers/article20/q2 ok puzzlers/article20/q2 (cached)

    2
  • benying
    2019-06-06
    打卡,201900606
    1
  • jxs1211
    2021-12-19
    修改b.N的值是由go运行时控制的吗,并不需要我们关注吧

    作者回复: 设置b.N是为了多次(N次)执行同一个被测函数,以便获得它的性能量化指标。一般情况下,你可以不去手动设置,但是应该知道它是做什么的。

  • moonfox
    2021-07-09
    问题:是否测试总时间一定会超过1秒? 答: 因为要找到1秒内最大可执行次数,而在这之前肯定要进行多次尝试,所以测试总时间应该总是会超过1秒的。 不知道这样理解是否正确呀

    作者回复: 当你用 b.N 做循环的时候,这个时间一般都会大于 1 秒的。但注意,这是对于单个测试函数而言的,不是测试总时间。

  • moonfox
    2021-07-09
    “最后再看这个执行次数的右边,2314 ns/op表明单次执行GetPrimes函数的平均耗时为2314纳秒。这其实就是通过将最后一次执行测试函数时的执行时间,除以(被测函数的)执行次数而得出的。” 根据文章所说, 500000是最后一次运行时的执行的次数,那可以倒推最后一次的运行时间 为 (2314 * 500000.0 ) / 1000 ^ 3 = 1.157秒,这个时间已经超过了默认时间上限1秒,与文章中说执行的次数是取最后一次不超过1秒的执行次数 相矛盾,对这点不是很理解
    展开

    作者回复: 文中还有一句话:“当某次执行的时间大于或等于上限时,我们就说这是命令此次对该测试函数的最后一次执行。”

    共 2 条评论
  • 传说中的成大大
    2020-03-27
    这两节的课程内容 我都不好总结 我唯一的感受就是 花了三篇文章来讲测试 说明 go语言的测试确实很重要
  • 张sir
    2020-02-29
    我怎么把t.Log的内容打印出来呢

    作者回复: 运行命令 go test -v

  • 手指饼干
    2019-10-08
    请问这里的500000,是最后一次执行测试函数时,被测函数的执行次数?还是整个测试周期,被测函数的执行次数?

    作者回复: 文章里写了: 当某次执行的时间大于或等于上限时,我们就说这是命令此次对该测试函数的最后一次执行。这时的b.N的值就会被包含在测试结果中,也就是上述测试结果中的500000。 2.本地无改动文件的跑了几遍都是显示时间的,没有那种显示cache的go test结果

  • xumc
    2019-03-07
    老师咨询个问题,本地go test的缓存能push到Git 的远程用来跑CI/CD吗

    作者回复: 不能,即使能也不应该这么做啊。

  • Caesar
    2019-01-23
    请问老师,假如我测试的时候要跳过某些包中的源码文件,该怎么写go test命令。网上搜了一圈也没找到。

    作者回复: 跳过测试函数的话,你可以看一下 -run 这个标记。如果要跳过某个包不指定那个包不就得了。

  • 虢國技醬
    2019-01-22
    打卡
  • A 凡
    2018-10-24
    之前一直就没有用心去了解测试命令中各个参数的含义,都是通过ide直接运行了,这里学到了