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语言实战与应用二)--学习笔记的更多相关文章

  1. Go语言核心36讲(Go语言基础知识三)--学习笔记

    03 | 库源码文件 在我的定义中,库源码文件是不能被直接运行的源码文件,它仅用于存放程序实体,这些程序实体可以被其他代码使用(只要遵从 Go 语言规范的话). 这里的"其他代码" ...

  2. Go语言核心36讲(Go语言实战与应用一)--学习笔记

    23 | 测试的基本规则和流程 (上) 在接下来的日子里,我将带你去学习在 Go 语言编程进阶的道路上,必须掌握的附加知识,比如:Go 程序测试.程序监测,以及 Go 语言标准库中各种常用代码包的正确 ...

  3. Go语言核心36讲(Go语言实战与应用三)--学习笔记

    25 | 更多的测试手法 在本篇文章,我会继续为你讲解更多更高级的测试方法.这会涉及testing包中更多的 API.go test命令支持的,更多标记更加复杂的测试结果,以及测试覆盖度分析等等. 前 ...

  4. Go语言核心36讲(Go语言实战与应用四)--学习笔记

    26 | sync.Mutex与sync.RWMutex 从本篇文章开始,我们将一起探讨 Go 语言自带标准库中一些比较核心的代码包.这会涉及这些代码包的标准用法.使用禁忌.背后原理以及周边的知识. ...

  5. Go语言核心36讲(Go语言实战与应用十二)--学习笔记

    34 | 并发安全字典sync.Map (上) 我们今天再来讲一个并发安全的高级数据结构:sync.Map.众所周知,Go 语言自带的字典类型map并不是并发安全的. 前导知识:并发安全字典诞生史 换 ...

  6. Go语言核心36讲(Go语言实战与应用十四)--学习笔记

    36 | unicode与字符编码 在开始今天的内容之前,我先来做一个简单的总结. Go 语言经典知识总结 在数据类型方面有: 基于底层数组的切片: 用来传递数据的通道: 作为一等类型的函数: 可实现 ...

  7. Go语言核心36讲(Go语言实战与应用十八)--学习笔记

    40 | io包中的接口和工具 (上) 我们在前几篇文章中,主要讨论了strings.Builder.strings.Reader和bytes.Buffer这三个数据类型. 知识回顾 还记得吗?当时我 ...

  8. Go语言核心36讲(Go语言实战与应用二十二)--学习笔记

    44 | 使用os包中的API (上) 我们今天要讲的是os代码包中的 API.这个代码包可以让我们拥有操控计算机操作系统的能力. 前导内容:os 包中的 API 这个代码包提供的都是平台不相关的 A ...

  9. Go语言核心36讲(Go语言实战与应用二十四)--学习笔记

    46 | 访问网络服务 前导内容:socket 与 IPC 人们常常会使用 Go 语言去编写网络程序(当然了,这方面也是 Go 语言最为擅长的事情).说到网络编程,我们就不得不提及 socket. s ...

随机推荐

  1. linux环境下,使用Navicat连接mysql时,提示本地IP无法连接虚拟环境下的mysql解决方案

    在Linux环境下,使用Navicat连接mysql时,提示本地IP无法连接虚拟环境下的mysql,提示如下: 而导致连接错误的原因是MYSQL没有开启远程登录权限. 解决方案: 在mysql命令中执 ...

  2. 服务器内部模拟Http请求

    前言: 在做小程序的开发时需要获取用户的openId用来做唯一标识,来获取对应用户的相关数据 官方的文档说明上有四个必须传的参数 其中appId和appSecret可在自己的微信公众号平台上获取,同时 ...

  3. 如何发现 Kubernetes 中服务和工作负载的异常

    大家好,我是来自阿里云的李煌东,今天由我为大家分享 Kubernetes 监控公开课的第二节内容:如何发现 Kubernetes 中服务和工作负载的异常. 本次分享由三个部分组成: 一.Kuberne ...

  4. Dapr + .NET Core实战(十二)服务调用之GRPC

    什么是GRPC gRPC 是一种与语言无关的高性能远程过程调用 (RPC) 框架. gRPC 的主要优点是: 高性能轻量级 RPC 框架. 协定优先 API 开发,默认使用协议缓冲区,允许与语言无关的 ...

  5. 题解 2020.10.24 考试 T3 数列

    题目传送门 题目大意 给出一个数 \(n\),你要构造一个数列,满足里面每个数都是 \(n\) 的因子,且每一个数与前面不互质的个数不超过 \(1\).问有多少种合法方案. 保证 \(n\) 的不同质 ...

  6. 好奇!仅 13kB 大小的游戏,源码长啥样?

    这个马赛克风格的表情正好 13Kb,有人竟然能用一个表情大小的空间,制作个游戏出来.我就不信这么点的地儿,能写出个花来?游戏能好玩吗?因为这些游戏点开就能玩,我抱着试一试的心态把玩了一会. 事实证明是 ...

  7. 源码解析-Abp vNext丨LocalEventBus

    前言 基础篇已经更新完了,从本篇开始我们进入,中级篇(学习部分源代码)我会挑一些我个人认为比较重要的知识点结合部分开源项目进行源码讲解,咱们废话不说直接上车. Abp vNext的事件总线分2种,一种 ...

  8. 【Azure 应用服务】App Service For Linux 如何在 Web 应用实例上住抓取网络日志

    问题描述 在App Service For Windows的环境中,我们可以通过ArmClient 工具发送POST请求在Web应用的实例中抓取网络日志,但是在App Service For Linu ...

  9. BPMN 學習實例

    什麼是業務流程圖? What is BPMN 業務流程建模符號(BPMN)是業務流程建模的一種方法.它基於統一建模語言(UML)中活動圖的概念,以圖形符號(業務流程圖)支持業務流程的規範.BPMN為企 ...

  10. python png图片生成gif

    有时候写代码就是这样别人把代码写好你在后面加一个句号就行了 我很懒不想写成函数,你自己来吧.有注释就不错了 这个依赖一个图像处理库pillow,轮子就是轮他不是车 import imageio imp ...