23 | 测试的基本规则和流程 (上)

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

从上个世纪到今日今时,程序员们,尤其是国内的程序员们,都对编写程序乐此不疲,甚至废寝忘食(比如我自己就是一个例子)。

因为这是我们普通人训练自我、改变生活、甚至改变世界的一种特有的途径。不过,同样是程序,我们却往往对编写用于测试的程序敬而远之。这是为什么呢?

我个人感觉,从人的本性来讲,我们都或多或少会否定“对自我的否定”。我们不愿意看到我们编写的程序有 Bug(即程序错误或缺陷),尤其是刚刚倾注心血编写的,并且信心满满交付的程序。

不过,我想说的是,人是否会进步以及进步得有多快,依赖的恰恰就是对自我的否定,这包括否定的深刻与否,以及否定自我的频率如何。这其实就是“不破不立”这个词表达的含义。

对于程序和软件来讲,尽早发现问题、修正问题其实非常重要。在这个网络互联的大背景下,我们所做的程序、工具或者软件产品往往可以被散布得更快、更远。但是,与此同时,它们的错误和缺陷也会是这样,并且可能在短时间内就会影响到成千上万甚至更多的用户。

你可能会说:“在开源模式下这就是优势啊,我就是要让更多的人帮我发现错误甚至修正错误,我们还可以一起协作、共同维护程序。”但这其实是两码事,协作者往往是由早期或核心的用户转换过来的,但绝对不能说程序的用户就肯定会成为协作者。

当有很多用户开始对程序抱怨的时候,很可能就预示着你对此的人设要崩塌了。你会发现,或者总有一天会发现,越是人们关注和喜爱的程序,它的测试(尤其是自动化的测试)做得就越充分,测试流程就越规范。

即使你想众人拾柴火焰高,那也得先让别人喜欢上你的程序。况且,对于优良的程序和软件来说,测试必然是非常受重视的一个环节。所以,尽快用测试为你的程序建起堡垒吧!

对于程序或软件的测试也分很多种,比如:单元测试、API 测试、集成测试、灰度测试,等等。我在本模块会主要针对单元测试进行讲解。

前导内容:go 程序测试基础知识

我们来说一下单元测试,它又称程序员测试。顾名思义,这就是程序员们本该做的自我检查工作之一。

Go 语言的缔造者们从一开始就非常重视程序测试,并且为 Go 程序的开发者们提供了丰富的 API 和工具。利用这些 API 和工具,我们可以创建测试源码文件,并为命令源码文件和库源码文件中的程序实体,编写测试用例。

在 Go 语言中,一个测试用例往往会由一个或多个测试函数来代表,不过在大多数情况下,每个测试用例仅用一个测试函数就足够了。测试函数往往用于描述和保障某个程序实体的某方面功能,比如,该功能在正常情况下会因什么样的输入,产生什么样的输出,又比如,该功能会在什么情况下报错或表现异常,等等。

我们可以为 Go 程序编写三类测试,即:功能测试(test)、基准测试(benchmark,也称性能测试),以及示例测试(example)。

对于前两类测试,从名称上你就应该可以猜到它们的用途。而示例测试严格来讲也是一种功能测试,只不过它更关注程序打印出来的内容。

一般情况下,一个测试源码文件只会针对于某个命令源码文件,或库源码文件(以下简称被测源码文件)做测试,所以我们总会(并且应该)把它们放在同一个代码包内。

测试源码文件的主名称应该以被测源码文件的主名称为前导,并且必须以“_test”为后缀。例如,如果被测源码文件的名称为 demo52.go,那么针对它的测试源码文件的名称就应该是 demo52_test.go。

每个测试源码文件都必须至少包含一个测试函数。并且,从语法上讲,每个测试源码文件中,都可以包含用来做任何一类测试的测试函数,即使把这三类测试函数都塞进去也没有问题。我通常就是这么做的,只要把控好测试函数的分组和数量就可以了。

我们可以依据这些测试函数针对的不同程序实体,把它们分成不同的逻辑组,并且,利用注释以及帮助类的变量或函数来做分割。同时,我们还可以依据被测源码文件中程序实体的先后顺序,来安排测试源码文件中测试函数的顺序。

此外,不仅仅对测试源码文件的名称,对于测试函数的名称和签名,Go 语言也是有明文规定的。你知道这个规定的内容吗?

所以,我们今天的问题就是:Go 语言对测试函数的名称和签名都有哪些规定?

这里我给出的典型回答是下面三个内容。

  • 对于功能测试函数来说,其名称必须以Test为前缀,并且参数列表中只应有一个*testing.T类型的参数声明。
  • 对于性能测试函数来说,其名称必须以Benchmark为前缀,并且唯一参数的类型必须是*testing.B类型的。
  • 对于示例测试函数来说,其名称必须以Example为前缀,但对函数的参数列表没有强制规定。

问题解析

我问这个问题的目的一般有两个。

  • 第一个目的当然是考察 Go 程序测试的基本规则。如果你经常编写测试源码文件,那么这道题应该是很容易回答的。
  • 第二个目的是作为一个引子,引出第二个问题,即:go test命令执行的主要测试流程是什么?不过在这里我就不问你了,我直接说一下答案。

我们首先需要记住一点,只有测试源码文件的名称对了,测试函数的名称和签名也对了,当我们运行go test命令的时候,其中的测试代码才有可能被运行。

go test命令在开始运行时,会先做一些准备工作,比如,确定内部需要用到的命令,检查我们指定的代码包或源码文件的有效性,以及判断我们给予的标记是否合法,等等。

在准备工作顺利完成之后,go test命令就会针对每个被测代码包,依次地进行构建、执行包中符合要求的测试函数,清理临时文件,打印测试结果。这就是通常情况下的主要测试流程。

请注意上述的“依次”二字。对于每个被测代码包,go test命令会串行地执行测试流程中的每个步骤。

但是,为了加快测试速度,它通常会并发地对多个被测代码包进行功能测试,只不过,在最后打印测试结果的时候,它会依照我们给定的顺序逐个进行,这会让我们感觉到它是在完全串行地执行测试流程。

另一方面,由于并发的测试会让性能测试的结果存在偏差,所以性能测试一般都是串行进行的。更具体地说,只有在所有构建步骤都做完之后,go test命令才会真正地开始进行性能测试。

并且,下一个代码包性能测试的进行,总会等到上一个代码包性能测试的结果打印完成才会开始,而且性能测试函数的执行也都会是串行的。

一旦清楚了 Go 程序测试的具体过程,我们的一些疑惑就自然有了答案。比如,那个名叫testIntroduce的测试函数为什么没执行,又比如,为什么即使是简单的性能测试执行起来也会比功能测试慢,等等。

demo52.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."
}

demo52_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)
}

总结

在本篇文章的一开始,我就试图向你阐释程序测试的重要性。在我经历的公司中起码有一半都不重视程序测试,或者说没有精力去做程序测试。

尤其是中小型的公司,他们往往完全依靠软件质量保障团队,甚至真正的用户去帮他们测试。在这些情况下,软件错误或缺陷的发现、反馈和修复的周期通常会很长,成本也会很大,也许还会造成很不好的影响。

Go 语言是一门很重视程序测试的编程语言,它不但自带了testing包,还有专用于程序测试的命令go test。我们要想真正用好一个工具,就需要先了解它的核心逻辑。所以,我今天问你的第一个问题就是关于go test命令的基本规则和主要流程的。在知道这些之后,也许你对 Go 程序测试就会进入更深层次的了解。

思考题

除了本文中提到的,你还知道或用过testing.T类型和testing.B类型的哪些方法?它们都是做什么用的?你可以给我留言,我们一起讨论。

笔记源码

https://github.com/MingsonZheng/go-core-demo

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

Go语言核心36讲(Go语言实战与应用一)--学习笔记的更多相关文章

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

    24 | 测试的基本规则和流程(下) Go 语言是一门很重视程序测试的编程语言,所以在上一篇中,我与你再三强调了程序测试的重要性,同时,也介绍了关于go test命令的基本规则和主要流程的内容.今天我 ...

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

    03 | 库源码文件 在我的定义中,库源码文件是不能被直接运行的源码文件,它仅用于存放程序实体,这些程序实体可以被其他代码使用(只要遵从 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. FastAPI logger日志记录方案 loguru模块

    实现方式: 采用 loguru 模块.跟flask直接挂载到app上有区别,当然也可以尝试去这样做. 但是 好像没有这个必要.要的就是个快速.整那些子虚乌有的东西完全木有意义. 1.首先是去项目git ...

  2. nginx与mysql安装

    yum install -y wget vim gcc-c++ bash-completion wget http://nginx.org/download/nginx-1.14.0.tar.gzta ...

  3. [转载]PHP命名规则

    PHP命名规则 引用地址:http://www.cnblogs.com/pengxl/p/3571157.html 就一般约定而言,类.函数和变量的名字应该是能够让代码阅读者能够容易地知道这些代码的作 ...

  4. 51nod1667-概率好题【容斥,组合数学】

    正题 题目链接:http://www.51nod.com/Challenge/Problem.html#problemId=1667 题目大意 两个人. 第一个人有\(k_1\)个集合,第\(i\)个 ...

  5. Redis之品鉴之旅(二)

    2)hash类型,上代码 using (RedisClient client = new RedisClient("127.0.0.1", 6379, "12345&qu ...

  6. AtCoder Beginner Contest 221 A~E题解

    目录 A - Seismic magnitude scales B - typo C - Select Mul D - Online games E - LEQ 发挥比较好的一场,就来搓篇题解. F ...

  7. CentOS 7.9+19c单实例静默安装

    一.环境准备 二.解压文件 三.文件配置 四.安装 五.相关调整 六.打补丁 一.环境准备0.依赖包安装 rpm -q --qf '%{NAME}-%{VERSION}-%{RELEASE} (%{A ...

  8. 从零入门 Serverless | Serverless 应用如何管理日志 & 持久化数据

    作者 | 竞霄 阿里巴巴开发工程师 本文整理自<Serverless 技术公开课>,关注"Serverless"公众号,回复"入门",即可获取 Se ...

  9. Unity——资源文件夹介绍

    Unity资源文件夹介绍 1.编辑时 在Asset文件下存在Resources和SteamingAsset文件夹: Resources 只读不可修改,打包时直接写死,没有办法通过热更新替换资源: 可以 ...

  10. 题解 [POI2013]SPA-Walk

    题目传送门 题目大意 给出两个长度为 \(n\) 的 \(01\) 串,问是否可以通过某一位把 \(s\) 变为 \(t\),但是中途不能变为 \(k\) 个 \(01\) 串中任意一个,问是否可行. ...