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

2021年11月22日 阅读数:1
这篇文章主要向大家介绍Go语言核心36讲(Go语言实战与应用一)--学习笔记,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

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

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

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

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

我我的感受,从人的本性来说,咱们都或多或少会否认“对自个人否认”。咱们不肯意看到咱们编写的程序有 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/ ),不得用于商业目的,基于本文修改后的做品务必以相同的许可发布。