Go语言中的单元测试(非常详细)
单元测试是指对程序中的最小可测试单元进行测试和验证,一个测试单元可以理解为一个函数、一个方法(或者一个类)等。
日常项目开发过程中,单元测试是必不可少的。Go 语言提供了单元测试标准库,我们可以很方便地通过该标准库实现基础测试、性能测试、示例测试、模糊测试以及分析代码覆盖率等。
首先需要强调一点,在 Go 语言中,单元测试文件必须以 x_test.go 格式命名,并且测试函数也必须以一定格式命名。go test 命令用于运行指定路径下的单元测试文件,该命令会编译单元测试文件,并运行对应的测试函数,以及输出测试结果。
我们举一个简单的单元测试示例,验证绝对值函数的正确性,如下所示:
通过 go test 命令执行上面的单元测试文件,结果如下所示:
参考上面的输出结果,单元测试执行成功,总耗时 0.687s。那么,Go 语言都支持哪几种类型的单元测试呢?主要分为基础测试、性能测试、事例测试、模糊测试以及代码覆盖率测试,这几种测试类型的详细信息可以通过 help 命令查看,如下所示:
参考上面的输出结果,基础测试的测试函数命名格式为 TestXxx,这种类型的单元测试通常用来判断结果是否符合预期,如果不符合可以使用 t.Errorf 输出错误提示;性能测试的测试函数命名格式为 BenchmarkXxx,这种类型的单元测试通常用来分析程序性能,输出结果会包含程序的运行次数以及平均耗时。
补充一下,性能测试的原理是多次执行一段程序,并计算程序的平均耗时,以此评估程序性能。性能测试的执行时间、执行次数等可以通过参数指定,支持的参数可以通过 help 命令查看,如下所示:
接下来以性能测试为例,介绍如何通过性能测试分析对比程序性能。
字符串应该是每一个 Go 开发者都比较熟悉的数据结构吧。在 Go 语言中,字符串是只读的,不能修改的,我们常用的字符串相加操作底层其实是通过申请内存与数据复制方式实现的,这种方式的性能其实是比较差的。
基于此,Go 语言在字符串库函数里为我们提供了 stringBuilder,我们可以通过它实现字符串相加操作,性能要比原始字符串相加操作好很多。我们可以通过性能测试验证一下这两者的性能差别,代码如下所示:
运行上面的单元测试,结果如下所示:
当然还有其他类型的单元测试,比如示例测试、模糊测试等,这里就不一一介绍了,有兴趣的读者可以自己编写一个测试示例来实践一下。
日常项目开发过程中,单元测试是必不可少的。Go 语言提供了单元测试标准库,我们可以很方便地通过该标准库实现基础测试、性能测试、示例测试、模糊测试以及分析代码覆盖率等。
首先需要强调一点,在 Go 语言中,单元测试文件必须以 x_test.go 格式命名,并且测试函数也必须以一定格式命名。go test 命令用于运行指定路径下的单元测试文件,该命令会编译单元测试文件,并运行对应的测试函数,以及输出测试结果。
我们举一个简单的单元测试示例,验证绝对值函数的正确性,如下所示:
// mall/test/demo_test.go func TestAbs(t *testing.T) { got := math.Abs(-1) if got != 1 { t.Errorf("Abs(-1) = %v, want 1", got) } }
通过 go test 命令执行上面的单元测试文件,结果如下所示:
go test test/demo_demo_test.go ok command-line-arguments 0.687s
参考上面的输出结果,单元测试执行成功,总耗时 0.687s。那么,Go 语言都支持哪几种类型的单元测试呢?主要分为基础测试、性能测试、事例测试、模糊测试以及代码覆盖率测试,这几种测试类型的详细信息可以通过 help 命令查看,如下所示:
go help testfunc //基础测试 A test function is one named TestXxx (where Xxx does not start with a lowercase letter) and should have the signature, func TestXxx(t *testing.T) { ... } //性能测试 A benchmark function is one named BenchmarkXxx and should have the signature, func BenchmarkXxx(b *testing.B) { ... } ......
参考上面的输出结果,基础测试的测试函数命名格式为 TestXxx,这种类型的单元测试通常用来判断结果是否符合预期,如果不符合可以使用 t.Errorf 输出错误提示;性能测试的测试函数命名格式为 BenchmarkXxx,这种类型的单元测试通常用来分析程序性能,输出结果会包含程序的运行次数以及平均耗时。
补充一下,性能测试的原理是多次执行一段程序,并计算程序的平均耗时,以此评估程序性能。性能测试的执行时间、执行次数等可以通过参数指定,支持的参数可以通过 help 命令查看,如下所示:
go help testflag //性能测试执行时间,默认 1 s;另外 Nx 可以设置测试的循环次数 -benchtime t //性能测试执行次数 -count n //设置逻辑处理器 P 的数目,默认等于 CPU 核数 -cpu 1,2,4 ......参考上面的输出结果:
- 参数 benchtime 可以设置单次性能测试的执行时间,默认为 1s,另外也可以通过 Nx(比如 -benchtime 100x)方式设置单次性能测试的循环次数;
- 参数 count 用于设置性能测试的执行次数;
- 参数 cpu 用户设置逻辑处理器 P 的数目,默认等于 CPU 核数。
接下来以性能测试为例,介绍如何通过性能测试分析对比程序性能。
字符串应该是每一个 Go 开发者都比较熟悉的数据结构吧。在 Go 语言中,字符串是只读的,不能修改的,我们常用的字符串相加操作底层其实是通过申请内存与数据复制方式实现的,这种方式的性能其实是比较差的。
基于此,Go 语言在字符串库函数里为我们提供了 stringBuilder,我们可以通过它实现字符串相加操作,性能要比原始字符串相加操作好很多。我们可以通过性能测试验证一下这两者的性能差别,代码如下所示:
func BenchmarkStringPlus(b *testing.B) { s := "" for i := 0; i < b.N; i++ { s += "abc" } } func BenchmarkStringBuilder(b *testing.B) { build := strings.Builder{} for i := 0; i < b.N; i++ { build.WriteString("abc") } }在上面的代码中,函数 BenchmarkStringPlus 用于测试原始字符串相加操作的性能,函数 BenchmarkStringBuilder 测试基于 Go 语言字符串库实现的字符串相加操作的性能。
运行上面的单元测试,结果如下所示:
go test test/demo_test.go -benchtime 100000x -count 2 -bench . BenchmarkStringPlus-8 100000 16614 ns/op BenchmarkStringPlus-8 100000 15138 ns/op BenchmarkStringBuilder-8 100000 3.434 ns/op BenchmarkStringBuilder-8 100000 4.791 ns/op参考上面的输出结果:
- 第一列表示测试函数(数字 8 表示执行当前测试函数时的逻辑处理器 P 的数目,本示例的结果理论上与逻辑处理器 P 的数目无关);
- 第二列表示单次性能测试的循环次数,第三列表示每次循环的平均耗时。
当然还有其他类型的单元测试,比如示例测试、模糊测试等,这里就不一一介绍了,有兴趣的读者可以自己编写一个测试示例来实践一下。