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

25 | 更多的测试手法

25 | 更多的测试手法-极客时间

25 | 更多的测试手法

讲述:黄洲君

时长14:29大小6.63M

在前面的文章中,我们一起学习了 Go 程序测试的基础知识和基本测试手法。这主要包括了 Go 程序测试的基本规则和主要流程、testing.T类型和testing.B类型的常用方法、go test命令的基本使用方式、常规测试结果的解读等等。
在本篇文章,我会继续为你讲解更多更高级的测试方法。这会涉及testing包中更多的 API、go test命令支持的,更多标记更加复杂的测试结果,以及测试覆盖度分析等等。

前导内容:-cpu 的功能

续接前文。我在前面提到了go test命令的标记-cpu,它是用来设置测试执行最大 P 数量的列表的。
复习一下,我在讲 go 语句的时候说过,这里的 P 是 processor 的缩写,每个 processor 都是一个可以承载若干个 G,且能够使这些 G 适时地与 M 进行对接并得到真正运行的中介。
正是由于 P 的存在,G 和 M 才可以呈现出多对多的关系,并能够及时、灵活地进行组合和分离。
这里的 G 就是 goroutine 的缩写,可以被理解为 Go 语言自己实现的用户级线程。M 即为 machine 的缩写,代表着系统级线程,或者说操作系统内核级别的线程。
Go 语言并发编程模型中的 P,正是 goroutine 的数量能够数十万计的关键所在。P 的数量意味着 Go 程序背后的运行时系统中,会有多少个用于承载可运行的 G 的队列存在。
每一个队列都相当于一条流水线,它会源源不断地把可运行的 G 输送给空闲的 M,并使这两者对接。
一旦对接完成,被对接的 G 就真正地运行在操作系统的内核级线程之上了。每条流水线之间虽然会有联系,但都是独立运作的。
因此,最大 P 数量就代表着 Go 语言运行时系统同时运行 goroutine 的能力,也可以被视为其中逻辑 CPU 的最大个数。而go test命令的-cpu标记正是用于设置这个最大个数的。
也许你已经知道,在默认情况下,最大 P 数量就等于当前计算机 CPU 核心的实际数量。
当然了,前者也可以大于或者小于后者,如此可以在一定程度上模拟拥有不同的 CPU 核心数的计算机。
所以,也可以说,使用-cpu标记可以模拟:被测程序在计算能力不同计算机中的表现。
现在,你已经知道了-cpu标记的用途及其背后的含义。那么它的具体用法,以及对go test命令的影响你是否也清楚呢?
我们今天的问题是:怎样设置-cpu标记的值,以及它会对测试流程产生什么样的影响?
这里的典型回答是:
标记-cpu的值应该是一个正整数的列表,该列表的表现形式为:以英文半角逗号分隔的多个整数字面量,比如1,2,4
针对于此值中的每一个正整数,go test命令都会先设置最大 P 数量为该数,然后再执行测试函数。
如果测试函数有多个,那么go test命令会依照此方式逐个执行。
1,2,4为例,go test命令会先以1,2,4为最大 P 数量分别去执行第一个测试函数,之后再用同样的方式执行第二个测试函数,以此类推。

问题解析

实际上,不论我们是否追加了-cpu标记,go test命令执行测试函数时流程都是相同的,只不过具体执行步骤会略有不同。
go test命令在进行准备工作的时候会读取-cpu标记的值,并把它转换为一个以int为元素类型的切片,我们也可以称它为逻辑 CPU 切片。
如果该命令发现我们并没有追加这个标记,那么就会让逻辑 CPU 切片只包含一个元素值,即最大 P 数量的默认值,也就是当前计算机 CPU 核心的实际数量。
在准备执行某个测试函数的时候,无论该函数是功能测试函数,还是性能测试函数,go test命令都会迭代逻辑 CPU 切片,并且在每次迭代时,先依据当前的元素值设置最大 P 数量,然后再去执行测试函数。
注意,对于性能测试函数来说,这里可能不只执行了一次。你还记得测试函数的执行时间上限,以及那个由b.N代表的被测程序的执行次数吗?
如果你忘了,那么可以再复习一下上篇文章中的第二个扩展问题。概括来讲,go test命令每一次对性能测试函数的执行,都是一个探索的过程。它会在测试函数的执行时间上限不变的前提下,尝试找到被测程序的最大执行次数。
在这个过程中,性能测试函数可能会被执行多次。为了以后描述方便,我们把这样一个探索的过程称为:对性能测试函数的一次探索式执行,这其中包含了对该函数的若干次执行,当然,肯定也包括了对被测程序更多次的执行。
说到多次执行测试函数,我们就不得不提及另外一个标记,即-count-count标记是专门用于重复执行测试函数的。它的值必须大于或等于0,并且默认值为1
如果我们在运行go test命令的时候追加了-count 5,那么对于每一个测试函数,命令都会在预设的不同条件下(比如不同的最大 P 数量下)分别重复执行五次。
如果我们把前文所述的-cpu标记、-count标记,以及探索式执行联合起来看,就可以用一个公式来描述单个性能测试函数,在go test命令的一次运行过程中的执行次数,即:
性能测试函数的执行次数 = `-cpu`标记的值中正整数的个数 x `-count`标记的值 x 探索式执行中测试函数的实际执行次数
对于功能测试函数来说,这个公式会更加简单一些,即:
功能测试函数的执行次数 = `-cpu`标记的值中正整数的个数 x `-count`标记的值
(测试函数的实际执行次数)
看完了这两个公式,我想,你也许遇到过这种情况,在对 Go 程序执行某种自动化测试的过程中,测试日志会显得特别多,而且好多都是重复的。
这时,我们首先就应该想到,上面这些导致测试函数多次执行的标记和流程。我们往往需要检查这些标记的使用是否合理、日志记录是否有必要等等,从而对测试日志进行精简。
比如,对于功能测试函数来说,我们通常没有必要重复执行它,即使是在不同的最大 P 数量下也是如此。注意,这里所说的重复执行指的是,在被测程序的输入(比如说被测函数的参数值)相同情况下的多次执行。
有些时候,在输入完全相同的情况下,被测程序会因其他外部环境的不同,而表现出不同的行为。这时我们需要考虑的往往应该是:这个程序在设计上是否合理,而不是通过重复执行测试来检测风险。
还有些时候,我们的程序会无法避免地依赖一些外部环境,比如数据库或者其他服务。这时,我们依然不应该让测试的反复执行成为检测手段,而应该在测试中通过仿造(mock)外部环境,来规避掉它们的不确定性。
其实,单元测试的意思就是:对单一的功能模块进行边界清晰的测试,并且不掺杂任何对外部环境的检测。这也是“单元”二字要表达的主要含义。
正好相反,对于性能测试函数来说,我们常常需要反复地执行,并以此试图抹平当时的计算资源调度的细微差别对被测程序性能的影响。通过-cpu标记,我们还能够模拟被测程序在计算能力不同计算机中的性能表现。
不过要注意,这里设置的最大 P 数量,最好不要超过当前计算机 CPU 核心的实际数量。因为一旦超出计算机实际的并行处理能力,Go 程序在性能上就无法再得到显著地提升了。
这就像一个漏斗,不论我们怎样灌水,水的漏出速度总是有限的。更何况,为了管理过多的 P,Go 语言运行时系统还会耗费额外的计算资源。
显然,上述模拟得出的程序性能一定是不准确的。不过,这或多或少可以作为一个参考,因为,这样模拟出的性能一般都会低于程序在计算环境中的实际性能。
好了,关于-cpu标记,以及由此引出的-count标记和测试函数多次执行的问题,我们就先聊到这里。不过,为了让你再巩固一下前面的知识,我现在给出一段测试结果:
pkg: puzzlers/article21/q1
BenchmarkGetPrimesWith100-2 10000000 218 ns/op
BenchmarkGetPrimesWith100-2 10000000 215 ns/op
BenchmarkGetPrimesWith100-4 10000000 215 ns/op
BenchmarkGetPrimesWith100-4 10000000 216 ns/op
BenchmarkGetPrimesWith10000-2 50000 31523 ns/op
BenchmarkGetPrimesWith10000-2 50000 32372 ns/op
BenchmarkGetPrimesWith10000-4 50000 32065 ns/op
BenchmarkGetPrimesWith10000-4 50000 31936 ns/op
BenchmarkGetPrimesWith1000000-2 300 4085799 ns/op
BenchmarkGetPrimesWith1000000-2 300 4121975 ns/op
BenchmarkGetPrimesWith1000000-4 300 4112283 ns/op
BenchmarkGetPrimesWith1000000-4 300 4086174 ns/op
现在,我希望让你反推一下,我在运行go test命令时追加的-cpu标记和-count标记的值都是什么。反推之后,你可以用实验的方式进行验证。

知识扩展

问题 1:-parallel标记的作用是什么?

我们在运行go test命令的时候,可以追加标记-parallel,该标记的作用是:设置同一个被测代码包中的功能测试函数的最大并发执行数。该标记的默认值是测试运行时的最大 P 数量(这可以通过调用表达式runtime.GOMAXPROCS(0)获得)。
我在上篇文章中已经说过,对于功能测试,为了加快测试速度,命令通常会并发地测试多个被测代码包。
但是,在默认情况下,对于同一个被测代码包中的多个功能测试函数,命令会串行地执行它们。除非我们在一些功能测试函数中显式地调用t.Parallel方法。
这个时候,这些包含了t.Parallel方法调用的功能测试函数就会被go test命令并发地执行,而并发执行的最大数量正是由-parallel标记值决定的。不过要注意,同一个功能测试函数的多次执行之间一定是串行的。
你可以运行命令go test -v puzzlers/article21/q2或者go test -count=2 -v puzzlers/article21/q2,查看测试结果,然后仔细地体会一下。
最后,强调一下,-parallel标记对性能测试是无效的。当然了,对于性能测试来说,也是可以并发进行的,不过机制上会有所不同。
概括地讲,这涉及了b.RunParallel方法、b.SetParallelism方法和-cpu标记的联合运用。如果想进一步了解,你可以查看testing代码包的文档

问题 2:性能测试函数中的计时器是做什么用的?

如果你看过testing包的文档,那么很可能会发现其中的testing.B类型有这么几个指针方法:StartTimerStopTimerResetTimer。这些方法都是用于操作当前的性能测试函数专属的计时器的。
所谓的计时器,是一个逻辑上的概念,它其实是testing.B类型中一些字段的统称。这些字段用于记录:当前测试函数在当次执行过程中耗费的时间、分配的堆内存的字节数以及分配次数。
我在下面会以测试函数的执行时间为例,来说明此计时器的用法。不过,你需要知道的是,这三个方法在开始记录、停止记录或重新记录执行时间的同时,也会对堆内存分配字节数和分配次数的记录起到相同的作用。
实际上,go test命令本身就会用到这样的计时器。当准备执行某个性能测试函数的时候,命令会重置并启动该函数专属的计时器。一旦这个函数执行完毕,命令又会立即停止这个计时器。
如此一来,命令就能够准确地记录下(我们在前面多次提到的)测试函数执行时间了。然后,命令就会将这个时间与执行时间上限进行比较,并决定是否在改大b.N的值之后,再次执行测试函数。
还记得吗?这就是我在前面讲过的,对性能测试函数的探索式执行。显然,如果我们在测试函数中自行操作这个计时器,就一定会影响到这个探索式执行的结果。也就是说,这会让命令找到被测程序的最大执行次数有所不同。
请看在 demo57_test.go 文件中的那个性能测试函数,如下所示:
func BenchmarkGetPrimes(b *testing.B) {
b.StopTimer()
time.Sleep(time.Millisecond * 500) // 模拟某个耗时但与被测程序关系不大的操作。
max := 10000
b.StartTimer()
for i := 0; i < b.N; i++ {
GetPrimes(max)
}
}
需要注意的是该函数体中的前四行代码。我先停止了当前测试函数的计时器,然后通过调用time.Sleep函数,模拟了一个比较耗时的额外操作,并且在给变量max赋值之后又启动了该计时器。
你可以想象一下,我们需要耗费额外的时间去确定max变量的值,虽然在后面它会被传入GetPrimes函数,但是,针对GetPrimes函数本身的性能测试并不应该包含确定参数值的过程。
因此,我们需要把这个过程所耗费的时间,从当前测试函数的执行时间中去除掉。这样就能够避免这一过程对测试结果的不良影响了。
每当这个测试函数执行完毕后,go test命令拿到的执行时间都只应该包含调用GetPrimes函数所耗费的那些时间。只有依据这个时间做出的后续判断,以及找到被测程序的最大执行次数才是准确的。
在性能测试函数中,我们可以通过对b.StartTimerb.StopTimer方法的联合运用,再去除掉任何一段代码的执行时间。
相比之下,b.ResetTimer方法的灵活性就要差一些了,它只能用于:去除在调用它之前那些代码的执行时间。不过,无论在调用它的时候,计时器是不是正在运行,它都可以起作用。

总结

在本篇文章中,我假设你已经理解了上一篇文章涉及的内容。因此,我在这里围绕着几个可以被go test命令接受的重要标记,进一步地阐释了功能测试和性能测试在不同条件下的测试流程。
其中,比较重要的有最大 P 数量的含义,-cpu标记的作用及其对测试流程的影响,针对性能测试函数的探索式执行的意义,测试函数执行时间的计算方法,以及-count标记的用途和适用场景。
当然了,学会怎样并发地执行多个功能测试函数也是很有必要的。这需要联合运用-parallel标记和功能测试函数中的t.Parallel方法。
另外,你还需要知道性能测试函数专属计时器的内涵,以及那三个方法对计时器起到的作用。通过对计时器的操作,我们可以达到精确化性能测试函数的执行时间的目的,从而帮助go test命令找到被测程序真实的最大执行次数。
到这里,我们对 Go 程序测试的讨论就要告一段落了。我们需要搞清楚的是,go test命令所执行的基本测试流程是什么,以及我们可以通过什么样的手段让测试流程产生变化,从而满足我们的测试需求并为我们提供更加充分的测试结果。
希望你已经从中学到了一些东西,并能够学以致用。

思考题

-benchmem标记和-benchtime标记的作用分别是什么?
怎样在测试的时候开启测试覆盖度分析?如果开启,会有什么副作用吗?
关于这两个问题,你都可以参考官方的go 命令文档中的测试标记部分进行回答。
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 10

提建议

上一篇
24 | 测试的基本规则和流程(下)
下一篇
26 | sync.Mutex与sync.RWMutex
unpreview
 写留言

精选留言(14)

  • 属雨
    2018-10-08
    第一个问题: -benchmem 输出基准测试的内存分配统计信息。 -benchtime 用于指定基准测试的探索式测试执行时间上限 示例: $ go test -bench=. word goos: linux goarch: amd64 pkg: word BenchmarkIsPalindrome-4 2000000000 0.00 ns/op PASS ok word 0.002s $ go test -bench=. -benchmem -benchtime 10s word goos: linux goarch: amd64 pkg: word BenchmarkIsPalindrome-4 10000000000 0.00 ns/op 0 B/op 0 allocs/op PASS ok word 0.003s 注意输出部分多的那两部分(0 B/op,0 allocs/op)以及执行次数。 第二个问题: 使用 -coverprofile=xxxx.out 输出覆盖率的out文件,使用go tool cover -html=xxxx.out 命令转换成Html的覆盖率测试报告。 覆盖率测试将被测试的代码拷贝一份,在每个语句块中加入bool标识变量,测试结束后统计覆盖率并输出成out文件,因此性能上会有一定的影响。 PS:使用-covermode=count标识参数将插入的标识变量由bool类型转换为计数器,在测试过程中,记录执行次数,用于找出被频繁执行的代码块,方便优化。
    展开
    共 1 条评论
    73
  • Sky
    2019-07-11
    -cpu=2,4 -count=2
    10
  • 晒太阳
    2019-09-27
    老师,回到MPG模型,我的理解是多级的关系,但M和P之间是否是一对多的关系,一个M对应着多个P,P对应着多个G(G队列runqueues),只有这样才能做到成千上万的G。我的理解对不对?M和P之间是否是一对多的关系?希望老师解惑,感谢啊。

    作者回复: M 和 P 是多对多的,或者说是动态结合的。一个 P 在不同时刻可能会对接不同的 M,反过来也是如此。而且 M 和 P 在数量上没有直接的关系。P 和 G 表面上说是一对多的,但其实一个 G 在它的生命周期中也可能会在不同 P 的可运行队列之间游走。更详细的东西,可以看我写的那本书。

    共 3 条评论
    6
  • KK
    2020-07-27
    如何对接口测试呢?其他语言中,比如php需要需要nginx作为代理处理请求,go是如何调试接口的呢?

    作者回复: 你说的是直接测试Web接口么?可以使用 net/http/httptest 这个包。

    4
  • 大王叫我来巡山
    2019-09-12
    慢慢的感觉就跟不上了,主要是还没有遇到这个应用的场景,评论也越来越少,不过老师确实很赞,不管是我提问题,还是吐槽,老师都很耐心的回答,但是我感觉不知道是不是我写的太少了,这个内容距离实践还有段距离

    作者回复: 确实需要多多练习。自己做项目吧。

    共 2 条评论
    3
  • MClink
    2022-07-08
    假设男女搭配才能干活,男生是G,女生是M,P就是男生的队列,有多条。每次有女生空闲了,就可以在P队列中拿一个男生和女生搭配,然后干活,干完后男生又随机回到某个队列,女生又等下一个男生,当女生不够多的时候,可以申请要多几个女生,因此每个女生都可能和不同的男生搭配干活,每个男生都可能和不同的女生搭配干活,也就是多对对的关系,不知道这样解释是否正确。

    作者回复: 哈哈哈,你这个解释够奇葩...不过,要是能促进知识记忆的话也未尝不可,哈哈哈,这些女生好累啊,男生还可以回队列歇一歇(我觉得要是男女角色互换的话应该效率更高^-^)...

    共 2 条评论
    2
  • wangkaiyuan
    2020-12-15
    老师,我想问一下,P大于M的时候,是不是会有P没有和M绑定,其中的G没有执行的机会呢

    作者回复: 这个绑定是动态的,择机绑,择机断,所以不会出现有可运行G但绑不上M的情况,只不过需要多等等(时间很短)罢了。另外M不够用的话,Go运行时系统会向OS申请创建新线程。

    1
  • poettian
    2020-11-19
    老师,这里有个疑惑:一个P在把G对接到M后,是会等待这个G运行结束或挂起后才会再次执行下一个G到M的对接吗?如果是,那我能理解设置多个P可以提高并发能力;如果不是,那不是只有一个P就够了,毕竟M数量是有限的。

    作者回复: 这个问题的答案不是“非0即1”的。 在G运行的过程中,相应的P要负责维护。所以,如果只有一个P那就相当于没有并发。 然而,当G等待锁或等待IO的时候,P会试图从可运行队列里再拿一个G。所以一个P可以同时负责好多个G的运行。 另外,Go调度器里还有共享的可运行G队列,其中的G可供所有的P获取,而且,P还可能会从其他P那里“偷”可运行G。因此,这个调度过程不是那么简单的。 你可以去看看我写的那本《Go并发编程实战》,对Go并发有一个更全面的了解。

    1
  • 現實
    2022-08-01 来自北京
    老师你的图是用什么软件画的呀?看着蛮不错的,可以分享一下吗

    作者回复: 工具很普通啊,用的就是 Power Point 。

  • 2022-03-26
    老师,我有个疑惑,希望能帮我解答一下。 文中有说:"通过-cpu标记,我们还能够模拟被测程序在计算能力不同计算机中的性能表现。" 在下面的示例结果中,虽然P数量不同,但是同一个测试函数中 被测函数的调用次数 几乎是一样的 所以我想问的是,在性能测试中,-cpu的参数设置 和 被测函数的调用次数之间有什么关联呢
    展开

    作者回复: 你说的“被测函数的调用次数”有些模糊,我不太清楚你想指的是什么(也许是我想多了)。如果按字面理解的话,那我的回答就是“它俩之间没有必然的关联关系”。 更具体地说,-cpu属于测试前设置,被测函数的执行次数属于测试运行时的一种测量(结果)。当然了前者可能会影响后者,但是可以影响后者的因素还有很多。 换句话说,在性能测试中,我们最终看的还是,被测函数在一段时间的执行次数(也就是单次执行的到底有多快)。而 -cpu 只是决定了一个前置条件而已,它并不会(也不能)充分决定被测函数的执行次数。

    1
  • 明远
    2022-03-23
    花了三章讲的全是命令行参数,具体的测试技巧和方法都没有提,怎么mock 断言,参数构造啥都没有说

    作者回复: 那些东西通过官方文档和博客都可以获得,何必再提。 你有啥问题可以直接在文章下面留言问。

  • 一步
    2021-11-08
    对于功能测试 -cpu 参数没有意义啊,为什么还会挨个遍历逻辑 CPU 切片执行多次呢?

    作者回复: -cpu 针对的是性能测试(基准测试)

  • 一步
    2021-11-07
    在性能测试 探索式执行达到执行时间上限的的过程中,对于被测试函数的执行时间会越来越大吗? 是由于资源竞争导致被测试函数的执行时间越来越大吗?

    作者回复: 时间一般是设定死的,变动的是你的程序在单位时间内的循环执行次数。 如果你的程序有内存泄露的话,肯定会越来越慢。程序正常运行导致的GC属于常规情况(在正式环境也不可能不GC啊)。 另外,如果你要是让 go test 并行运行多个性能测试,那么就需要考虑互相干扰的问题。性能测试最好还是独占运行,而且颗粒度越小越好,这样才利于发现问题和记录最严谨的运行数据。

  • Lucas WANG
    2018-10-09
    Go语言都有哪些框架?我查了一下,貌似只有Web框架?
    共 2 条评论