Go语言学习笔记(十五)
一、测试和性能
1 测试:软件开发最重要的方面
测试软件程序可能是软件开发人员能够做的最重要的事情。通过测试代码的性能,开发人员能够以在很大程度上确定程序是有效的。另外,每次修改代码后,卡法人员都可以运行测试,确认没有引入Bug和衰退。通过测试软件,还能让软件工程师确认程序按期望的方式工作。
通常软件测试是从概念功能的用户故事或规范衍生而来的。例如,如果有一个用户故事,指出一个函数接收两个数字,将它们相加并返回结果,就可以轻松地编写对此进行检查的测试。有些项目还要求新代码有配套的测试。
编写良好的测试可充当文档。鉴于测试描述了程序期望的运行方式,新加入项目的开发人员通常可通过阅读测试来了解程序的运行方式。
常用的测试有多种:
1.单元测试
2.功能测试
3.集成测试
1.1 单元测试
单元测试针对一小部分代码,并独立地对它们进行测试。通常,这一小部分代码可能是单个函数,而要测试的是其输入和输出。典型的单元测试可能指出,如果给函数提供这些值,他应返回这个值。在确认程序最小的构建按期望的方式运行方面这种测试很有用。在程序增大和变化过程中,单元测试是发现衰退的绝佳方式。衰退是修改过程中引入的Bug或错误
衰退意味着代码在修改前有效,但修改后无效了。单元测试通常能够发现衰退,因为他们测试的是程序的最小组成部分
1.2 集成测试
集成测试通常测试的是应用程序各部分协同工作的情况。如果单元测试检查的是程序的最小组成部分,那么集成测试检查的就是应用程序哥哥组件协同工作的情况。集成测试还检查诸如网络调用和数据库连接等方面,以确保整个系统按照期望的那样工作。同城,集成测试比单元测试更难编写,因为这些测试需要评估应用程序依赖的各个部分
1.3 功能测试
功能测试通常被称为端到端测试或由外向内的测试。这些测试从用户的角度核实软件按期望的那样工作,他们评估从外部看到的程序的运行状况,而不关心软件内部的工作原理。对用户来说,功能测试可能是最重要的测试。下面是一些功能测试的例子:
- 测试命令行工具,确定在用户提供特定的输入时,他将显示特定的输出。
- 对网页运行自动化测试
- 对API运行从外到内的测试,并检查相应代码和报头
1.4 测试驱动开发
很多开发人员都提倡采用测试驱动开发(TDD)。这种做法从测试的角度考虑新功能,先编写测试来描述代码片段的功能,在着手编写代码。这有很多优点
- 有助于描述代码设计,因为考虑清楚代码片段的工作原理后,可改善代码设计。
- 有助于提供有关功能工作原理的定义
- 未来可使用现成的测试来确定没有发生衰退
- 可使用现成的测试来核实正确地实现了代码
通过采用TDD,工程师可改善设计,并根据测试得以通过来确认代码是有效的。
2 testing包
为支持测试,GO语言在标准库中提供了testing包,它还支持命令go。与Go语言的其他众多方面一样,GO语言中也有与testing包相关的设计良好的约定。
第一个约定
- 第一个约定是,Go测试与其测试的代码放在一起。测试不是放在独立的测试目录中,而是与他们要测试的代码放在同一个目录中。测试文件是这样命名的:在要测试的文件的名称后面加上后缀_test,因此如果要测试的文件名为
string.go
,则测试他的文件将名为strings_test.go
,并位于文件strings.go
所在的目录中(如下图:)
第二个约定
- 第二个约定是,测试为名称以
Test
打头的函数,如下面这个例子package example01 import "testing" func TestTruth(t *testing.T) { if true != true { t.Fatal("The world id crumbling") } }
在这里导入了testing包
函数名TestTruth标明这是一个测试,因为它以单词Test开头
向这个函数传递了类型T,它包含很多用于测试代码的函数
使用if语句判断true是否与它自身相等
这条if语句的结果为true,测试将通过运行程序,结果如下:
对了这里example01
的目录结构是这样的example01
example01.go
example01_test.goexample01.go中就只有package example01这一句话
如果将测试代码里的trueb变为false,我们再来看看有什么结果:
测试失败时,命令go test提供了一些很有用的信息:测试名、文件名以及导致测试失败的代码所在行。这些信息有助于避免测试失败,因为它们明确的指出了问题出在什么地方。
第三个预定
- 第三个约定是,在测试包中创建两个变量:got和want,它们分别表示要测试的值以及期望的值。
如下面两个程序所示
这个测试用来判断Greeting返回的字符串是否符合预期,测试结果如下:
在这里,我使用了go test -v example02_test.go example02.go
,是因为会报编译错误
详细解释在这里
这个实例也:说明了testing包got want测试模式的神奇魅力可指定期望输出
可通过测试确定情况发生了变化
3 运行表格驱动测试
通常,函数和方法的响应随收到的输入而异,在这种情况下,如果每个测试只使用一个值,将导致大量的代码。如下
//根据输入显示不同的输出
package example03
func translate(s string) string {
switch s {
case "en-US":
return "Hello "
case "fr-FR":
return "Bonjour "
case "it-IT":
return "Ciao "
default:
return "Hello "
}
}
func Greeting(name, locale string) string {
salutation := translate(locale)
return (salutation + name)
}
测试这个函数时,如果像
2
那样在每个测试中只使用一个值,则测试每个条件时都将包含大量重复的代码,如下:
package example03
import "testing"
func TestFrTranslation(t *testing.T) {
got := translate("fr")
want := "Bonjour "
if got != want {
t.Fatalf("Expected %q, got %q", want, got)
}
}
func TestUSTranslation(t *testing.T) {
got := Greeting("George", "en-US")
want := "Hello George"
if got != want {
t.Fatalf("Expected %q, got %q", want, got)
}
}
面对这种情况我们可以使用表格驱动预测:
所谓的表格驱动说穿了就是将所有的输入和输出提前储存到一张表(建立对应的结构体)里,之后我们可以按照表格的index
进行输入输出的索引对应:代码如下
package example04
import "testing"
type GreetingTest struct {
name string
locale string
want string
}
var greetingTests = []GreetingTest{
{"George", "en-US", "Hello George"},
{"Chloé", "fr-FR", "Bonjour Chloé"},
{"Giuseppe", "it-IT", "Ciao Giuseppe"},
}
func TestGreeting(t *testing.T) {
for _, test := range greetingTests {
got := Greeting(test.name, test.locale)
if got != test.want {
t.Errorf("Greeting(%s,%s) = %v; want %v", test.name, test.locale, got, test.want)
}
}
}
- 创建一个结构体,用于存储编写测试所需要的数据,包括输入和期望输出。
- 创建一个有结构体组成的切片,用于存储要测试的所有情形,包括期望输出。
- 在测试中,遍历切片中的所有结构体,并测试实际输出是否与期望输出相同
- 只要有测试失败,就像控制台打印一条消息。
- 测试结果如下:
4 基准测试
在前面的学习中,我们了解到字符串以及我们应该如何拼接它们(赋值、运算符、join以及使用缓冲区)。其中使用缓冲区拼接字符串效率最高,这一点可以通过基准测试来证明:
Go语言提供了功能强大的基准此时框架,能够让我们使用基准测试程序来确定完成特定任务时间性能最佳的方式是哪一种。下面程序给了三种拼接字符串的方式:
package example05
import (
"bytes"
"strings"
)
func StringFromAssignment(j int) string {
var s string
for i := 0; i < j; i++ {
s += "a"
}
return s
}
func StringFromAppendJoin(j int) string {
s := []string{}
for i := 0; i < j; i++ {
s = append(s, "a")
}
return strings.Join(s, "")
}
func StringFromBuffer(j int) string {
var buffer bytes.Buffer
for i := 0; i < j; i++ {
buffer.WriteString("a")
}
return buffer.String()
}
这些函数根据传入的整数值生成相应长度的字符串。函数功能完全相同,性能的比较我们可以使用基准测试方法;
testing包含的基准测试框架可以帮助我们反复的运行函数,从而建立基准。基准测试框架将通过调整函数的执行次数来获得可靠的数据集。基准测试结束后,将生成一个报告,指出每次操作的耗时(ns)
基准测试一关键词Benchmark开头,它们接受一个类型为B的参数,并对函数进行基准测试:如下
package example05
import "testing"
func BenchmarkStringFromAssignment(b *testing.B) {
for n := 0; n < b.N; n++ {
StringFromAssignment(100)
}
}
func BenchmarkStringFromAppendJoin(b *testing.B) {
for n := 0; n < b.N; n++ {
StringFromAppendJoin(100)
}
}
func BenchmarkStringFromBuffer(b *testing.B) {
for n := 0; n < b.N; n++ {
StringFromBuffer(100)
}
}
测试结果如下:
5提供覆盖测试率
测试覆盖率是度量代码测试详尽程度的指标,他只出了被测试的代码所在的百分比值。
返回第一个例子,如果我们在第一个包中添加了一个函数如下:
package example06
func Greeting(s string) string {
return ("Hello " + s)
}
func Farewell(s string) string {
return ("Goodbye " + s)
}
如果我们的测试没有改变
package example06
import "testing"
func TestGreeting(t *testing.T) {
got := Greeting("George")
want := "Hello George"
if got != want {
t.Fatalf("Expected %q, got %q", want, got)
}
}
它将只测试第一个函数,此时我们可能会误以为整个代码都是正确的,为了避免这个问题,testing提供了标志-cover,可指出测试覆盖率,实验结果如下:
上述输出标明,测试只覆盖了50%的代码。最佳目标是实现100%的覆盖,但这样的目标并非总能实现,因为对于有些代码,要对其进行测试很难。
6 相关问答
6.1 我们应该以什么样的频率运行测试?
理想情况下,每次提交代码前都应该运行测试。有些开发人员在每次保存文件后都运行测试,但这可能比较麻烦,而且会分散注意力。提交代码前是运行测试的最佳时机。
6.2 应该达到多大的测试覆盖率?
实现100%的测试覆盖率是一个值的为之努力的目标,但对大型项目而言,这几乎是不可能的。达到80%左右的测试覆盖率就可以了,具体多少取决于项目的复杂度。如果能达到100%的覆盖率,就这样去做好了
6.3 应将测试文件放在什么地方?
测试文件和被测试文件在同一个目录下,但是测试文件的名字后面加了_test
6.4 何为测试驱动开发(TDD)?
测试驱动开发是这样一种做法,即先编写无法通过的测试来描述期望的功能,在着手编写代码。很多开发人员认为,这样做可以提高代码设计质量,确保测试覆盖率成为开发过程的有机组成部分。
6.5 基准测试的好处?
基准测试采用客观的经验方法来确定性能。使用基准测试意味着有可反复运行的测试,可用来确定一个函数的速度比另一个函数快,或者修改代码后性能提高或降低了。
参考书籍
[1]: 【Go语言入门经典】[英] 乔治·奥尔波 著 张海燕 译
参考链接:
golang test测试使用
go test命令(Go语言测试命令)完全攻略
go test 用法总结
go test 测试单个文件和测试单个函数