github email
Test-driven development with Go

介绍

使用Go语言进行驱动开发。

讲什么

本书将讲解如何使用一个在线服务创建二维码,仅仅使用Go自带的框架。

二维条形码可以编写各种信息。许多智能手机都用扫描二维码的应用程序,你可能以前看见过,如果没有,下面就是典型的二维码信息:

qrcode

在本书的结尾将由一个完整的例子,使用http服务来创建二维码。在此过程中,你讲学习到Go语言的一些特性和其标准库用法以及测试驱动开发。

预期读者

本书的读者需要有一定的编程基础,如果没有学习过Go,请点击此处进行学习。

要求

在阅读本书前,你需要准备以下工具:

Go

Go语言环境是本书中的唯一要求。你可以从其官网进行下载安装。如果你的操作系统是OS X,可以使用Homebrew进行安装。 大多数的Linux都已经在包管理器中加入了Go。

一个终端

使用Go语言的最佳途径就是使用终端。大多数的Linux系统使用其自动的终端shell,苹果用户可以使用Terminal.app;而 windows用户可以使用Cygwin或Git Bash。

一个编辑器

最终,你将使用一个编辑器来进行编码。编辑器有非常多的选择,然而Sublime Text看起来非常流行。默认的 Go配置插件,Emasc和Vim都有提供。

翻译者:推荐Visual Studio Code,好用,免费。

代码实例

Go代码

本书包含大量代码示例。 Go代码是最突出的,这样的代码总是用文件名和行号注释; 这些数字不一定从1开始,但在写入时请参考文件中的位置。 每个片段都包含文件名作为其标题,通常后面跟着一些纯文本注释。

fragment.go

17 func (r *Receiver) MyFunc() err {
18 	print("Anything")
19 	print("Even more")
20 }

17

关于第17行的一些有趣信息。

17 - 20

关于从第17行到第20行的整个代码块的一些有趣信息。

有时代码样本缺少行号 - 在这种情况下,代码实际上并不包含在源代码中,而是意味着要点。

go fmt 高亮显示运算符优先级

func Hypothetical(a b c int) int {
	return 4*a*a + (b*c)/2
}

命令行会话

软件开发是编辑器和命令行之间的相互作用。相关命令行会话也包含在本书中,并呈现如下:

交互式命令行会话

$ echo "Hello world"
Hello world

$ cat HelloWorld.txt
cat: HelloWorld.txt: No such file or directory

命令行会话没有行号,并始终使用$来指示提示。

目录列表

在少数情况下,我必须参考目录列表。 它们始终以工作空间为根,即存储源代码的目录的通用名称。

目录结构

workspace/
├── bin/
   └── qrcoded 
└── src/
    └── github.com/
        └── publysher/
            └── qrcoded/
                └── qrcoded.go

获取代码

本书中的所有源代码都发布在http://github.com/publysher/golang-tdd; master分支包含最终结果,每个章节都有一个单独的分支。 源代码为开源许许可。

你好,世界

在计算机编程的传统中,我将从一个简单的“Hello World”程序开始。使用这个程序,我将向您介绍GO语言工具。

你的第一个程序

编写Go从创建新工作区开始,该工作区将存储所有Go代码。 启动编辑器,在工作区中创建一个名为qrcoded.go的新文件,并确保它看起来像这样:

qrcoded.go

1 package main
2 
3 import "fmt"
4 
5 func main() {
6 	fmt.Println("Hello QR Code")
7 }

如果你已经完成了GO2的在线旅行,这个程序不会对你造成任何神秘感。然而,在GO之旅使用交互式GO Playground时,本书将使用命令行编译、测试和运行程序。

编译程序

GO是一种编译语言,运行该程序需要首先编译它。这是使用GO构建命令完成的,它将程序编译成一个可执行文件。可执行文件的名称与包含主函数的GO源文件的名称相同。在这种情况下,得到的二进制被称为qrcoded。

使用 go build

$ go build qrcoded.go

$ ls
qrcoded		qrcoded.go

$ ./qrcoded
Hello QR Code

go工具是Go生态系统的中心。 它定义了许多用于管理软件开发的命令,从而无需任何类型的外部构建管理工具。 其中一个命令是clean命令,它从工作区中删除所有已编译的二进制文件:

使用 go clean

$ ls
qrcoded		qrcoded.go

$ go clean

$ ls
qrcoded.go

go工具有更多的子命令,我将在本书中介绍其中的大部分内容。 此时需要特别提及这个命令:go help,它描述了所有可用的命令,然后运行。

在前面的示例中,您可能已经注意到两件事。 首先,“构建,执行,清理”循环有些重复。 其次,您可能已经注意到go build命令很快。 这不仅仅是因为您正在编译一个简单的程序-Go编译器的速度非常快。 实际上,快速编译周期是Go设计目标的重要组成部分,许多设计都受到了这一目标的影响。

Go团队利用编译器的速度使程序运行更简单:

使用 go run

$ go run qrcoded.go
Hello QR Code

go语言样式和go fmt命令

大多数语言在风格问题上一直争论不休,比如空间与标签的优点、花括号的最佳位置以及进口的排序。虽然这样的辩论有时会很有趣,但却没有效果。

GO设计人员对首选代码风格采取了坚定的立场,定义了一种为版本控制系统和代码审查优化的样式。但不是把它写在一个尘土飞扬的互联网角落,而是为我们提供了go fmt工具:

固定格式化问题

$ go fmt qrcoded.go
qrcoded.go

你可能对Go做出的选择,有理论上的、美学上的甚至宗教上的反对,但是最好还是去fmt代码。如果只是为了防止无用的辩论。

QR代码雏形

“Hello World”是一个很好的开始方法,但它仍然有很长的路要走,最终的目标:生成QR码。让我们来看看下一步,看看QR码产生的程序是什么样子:

qrcoded.go

 1 package main
 2 
 3 import (
 4 	"fmt"
 5 	"io/ioutil"
 6 )
 7 
 8 func main() {
 9 	fmt.Println("Hello QR Code")
10 
11 	qrcode := GenerateQRCode("555-2368")
12 	ioutil.WriteFile("qrcode.png", qrcode, 0644)
13 }

11

使用经典的软件工程技术,称为一厢情愿的想法,我假设存在一个函数,生成一个给定的输入字符串的QR码。在这种情况下,输入是电话号码555-2368,预期输出是一个编码的PNG,看起来像这样:

qrcode

12

在当前目录下,我使用io/ioutil包来轻松地创建一个名为qrcode.png的文件。

当然,这个代码并不能编译通过:

一厢情愿的想法:

$ go run qrcoded.go
./qrcoded.go:11: undefined: GenerateQRCode

让我们用最少的必要量来扩展代码:一个名为的GenerateQRCode函数,它接受一个字符串并返回一个字节切片。

qrcoded.go

15 func GenerateQRCode(code string) []byte {
16 	return nil
17 }

这足以让程序再次运行。

创建QR码

$ go run qrcoded.go 
Hello QR Code

$ ls
qrcode.png	qrcoded.go

$ file qrcode.png
qrcode.png: empty

成功! 我们的代码距离生成QR码更近了一步。

从这里开始,您可以慢慢充实GenerateQRCode函数; 在每次迭代中,您可以运行程序并检查生成的文件,方法是使用file命令或在图像编辑器中打开它,并直观地验证是否生成了正确的QR代码。

这是一种非常有效的方法,但我相信有更好的方法。 它被称为测试驱动开发,它是整本书的主题。 继续下一章,看看它的实际效果。

本章总结

在本章中,我介绍了go命令及其中一些最重要的子命令:

go build

将程序编译为可执行文件。

go clean

删除所有已编译的可执行文件。

go run

编译程序,执行它,并清理生成的可执行文件。

我还扩展了基础程序,提供了一个生成条形码的最小结构,为本书的核心部分铺平道路:Go中的测试驱动开发。

测试驱动开发

测试驱动开发是创建模块化、设计良好、可测试代码而不做任何预先设计的严格规则。它通过使您在非常短的周期中工作来实现这一目标:创建自动化测试,编写最小代码量以满足该测试,并重构代码以提高质量。这种方法确保了所得到的代码的几个优良特性:

  1. 从测试开始,迫使你思考预期的行为;
  2. 基于测试编写代码,迫使您编写可测试代码;
  3. 立即重构结果会迫使你思考代码,以及如何在更大的范围内适应代码。

在本章中,我将介绍如何用Go创建自动化测试套件,我将提供两个示例,展示测试、编写代码、重构周期。

使用go编写测试用例

编写测试完全集成在Go测试工具中。这个命令搜索匹配*_test.go的文件,并执行与特定模式匹配的所有函数。让我们看一下Go测试:

运行go test

$ go test
?	_/workspace/golang-tdd	[no test files]

正如您所看到的,Go测试抱怨测试文件不存在。这是正确的,因为没有匹配*_test.go的任何文件。因此,继续创建一个名为qrcoded_test.go的文件:

qrcoded_test.go

1 package main

当再次运行go test时,输出略有不同:

使用空测试文件运行测试 go test
$ go test
testing: warning: no tests to run
PASS
ok  	_/workspace/golang-tdd	0.018s

该工具不再抱怨缺少测试文件,但它确实没有测试函数。 当然,这是正确的,因为我们没有编写任何测试函数。

在Go中编写单元测试是一个简单的事情,即创建一个看起来像func TestXxx(t * testing.T)的函数,其中Xxx是您选择的名称。 运行go test时,匹配此模式的每个函数都是独立执行的。 使用testing.T中定义的方法,你可以告知测试运行器有关测试失败的信息。

让我们通过第一个测试扩展qrcoded_test.go:验证GenerateQRCode返回一个有用的值。

qrcoded_test.go
 1 package main
 2 
 3 import (
 4 	"testing"
 5 )
 6 
 7 func TestGenerateQRCodeReturnsValue(t *testing.T) {
 8 	result := GenerateQRCode("555-2368")
 9 
10 	if result == nil {
11 		t.Errorf("Generated QRCode is nil")
12 	}
13 	if len(result) == 0 {
14 		t.Errorf("Generated QRCode has no data")
15 	}
16 }

7-16

该块定义了单元测试; 函数签名与func TestXxx(t * testing.T)匹配,正文将作为单个测试执行。

8

此行是测试的本质:使用特定数据调用GenerateQRCode函数,并存储结果以供后续检查。

10-15

这些行包含测试的断言。 检查结果,如果结果未定义或为空,则通过调用t.Errorf通知测试工具失败。

那么让我们看看如果我们再次运行测试 go test会发生什么:

使用失败的单元测试运行测试 go test
$ go test 
--- FAIL: TestGenerateQRCodeReturnsValue (0.00 seconds)
	qrcoded_test.go:11: QRCode is nil
	qrcoded_test.go:14: QRCode has no data
FAIL
exit status 1
FAIL	_/workspace/golang-tdd	0.018s

这次测试清楚地运行了新的单元测试。 正如预期的那样,它失败了。 该框架清楚地显示了失败的函数的名称,确切的行和测试中提供的确切消息。

此时,是时候编写将使此测试工作的最少量代码:

qrcoded.go
15 func GenerateQRCode(code string) []byte {
16 	return []byte{0xFF}
17 }

GenerateQRCode的新实现仍然非常无趣,但它足以满足测试用例:

通过所有测试
$ go test 
PASS
ok  	_/Users/yigalduppen/src/golang-tdd	0.018s

这次测试通过了。 这本身并不意味着什么 - GenerateQRCode的实现仍然不会产生任何QR码。 但是我们现在有了坚实的基础来实践一些真正的测试驱动开发,并慢慢测试我们的方式到一个正常运行的实现。

红(Red),绿(Green),重构(Refactor)

在上一节中,我为之前未测试的代码创建了一个测试。 这非常有用,但它与测试驱动开发不同。 在本章开头,我解释了测试驱动开发如何以明确定义的循环为特征,通常称为Red/Green/Refactor循环:

红 Red

循环开始于编写捕获新要求的测试,预计此测试将失败。许多工具以红色显示测试失败,因此得名。

绿 Green

通过编写满足测试所需的最少量代码来继续循环。 这个名称也源于许多工具以绿色显示测试成功的事实。 当您开始练习测试驱动的开发时,编写超过最少量的代码是一个常见的陷阱。 请注意这一点,并不断问自己是否所做的工作超过了最低要求。

重构 Refactor

循环中的最新步骤是使测试驱动开发成为可行的过程:它迫使您退后一步,查看代码,并在不添加任何功能的情况下改进其结构。重构步骤不是可选步骤,如果没有这一步骤,你的代码将迅速退化为,经过良好测试,但难以理解的混乱。

让我们通过提出一个新的要求来看看,现实世界中的红/绿/重构循环,GenerateQRCode函数返回的字节切片,应该代表一个有效的PNG图像。

红 Red

第一步是创建一个捕获此要求的新测试。从广义上讲,有两种方法可以解决这个问题:你可以检查前八个字节是否与PNG的头匹配,或者你可以继续解码图像,如果在解码过程中发生错误,你就知道该字节切片并不代表PNG。

如果你是一个偏执的读者,你可能已经在这个推理中发现了一个缺陷:如果解码算法包含一个错误会导致完全有效的PNG错误怎么办?这是一个有效的问题,它表明了为什么测试驱动的开发不是错误的神奇解决方案。测试驱动开发是一种开发软件的技术;你将不得不假设范围之外的依赖项(例如Go标准库)按照广告的方式工作。

这个假设在本书中进行,这就是为什么下一个测试使用解码,看看它是否正在工作的原因:

qrcoded_test.go
13 func TestGenerateQRCodeGeneratesPNG(t *testing.T) {
14 	result := GenerateQRCode("555-2368")
15 	buffer := bytes.NewBuffer(result)
16 	_, err := png.Decode(buffer)
17 
18 	if err != nil {
19 		t.Errorf("Generated QRCode is not a PNG: %s", err)
20 	}
21 }

1-12

为了使代码示例保持不变,我将经常只显示文件的相关部分。 我还将省略代码引入的任何导入。 在这种情况下,你看不到的导入是bytes和image/png。

13-21

这里我介绍了一种全新的测试函数。这不是红色/绿色/重构周期严格要求的,扩展现有测试以捕获新功能也是完全有效的。

16

这行包含解码逻辑:我解码字节数组,放弃任何积极的结果,并重点放在错误。接下来的三行验证此错误没有发生。注意PNG。解码在字节切片上不工作,但满足io.Reader接口的类型。这就是为什么行15将字节块封装在字节表缓冲区中的原因。

了解GenerateQRCode函数的当前状态,可以认为这会使测试失败。然而,在你确定之前不要做任何事情:

开始红测试
$ go test 
--- FAIL: TestGenerateQRCodeGeneratesPNG (0.00 seconds)
	qrcoded_test.go:26: Generated QRCode is not a PNG: unexpected EOF
FAIL
exit status 1
FAIL	_/workspace/golang-tdd		0.020s

这是预期的结果,现在是时候用最少量的代码扩展GenerateQRCode函数来满足这个测试。

绿 Green

实现此新功能遵循与我们的测试相同的模式:创建图像并使用png.Encode对其进行编码。 再一次,我假设png.Encode的工作方式与宣传的一样。 切勿在单元测试中测试外部依赖项。

qrcoded.go
18 func GenerateQRCode(code string) []byte {
19 	img := image.NewNRGBA(image.Rect(0, 0, 21, 21))
20 	buf := new(bytes.Buffer)
21 	_ = png.Encode(buf, img)
22 
23 	return buf.Bytes()
24 }

如果您是测试驱动开发的新手,那么这种实现可能会让人感到作弊。 毕竟,我们只是在运行Go标准库,假设它正常工作,而不是做任何实际工作。 但是,在几周内,当GenerateQRCode的内部变得无法识别时,即使实现完全改变,测试仍将是有效的测试。

你练习测试驱动的开发越多,你就越能摆脱这种欺骗的感觉。 因为测试驱动的开发需要你做小步骤,所以每一个实现都会感觉微不足道。 真正的价值不在于步骤本身,而在于最终产品。

即使我做了一个微不足道的改变,这肯定会起作用,你应该总是检查结果:

开始绿测试
$ go test 
PASS
ok  	_/workspace/golang-tdd		0.021s

重构

循环中的最后一步是重构步骤。我之前已经说过了,我会再说一遍:在测试驱动开发中,重构步骤不是可选的。没有它,你正在练习测试第一次开发,这需要一个良好的前期设计。如果你如此倾向,可以将重构步骤看作是测试驱动开发的主要指示。

每个重构步骤从一个问题开始:在不改变功能的情况下,如何使代码更好地表达它的意图?

在这种情况下,即使代码量仍然非常有限,也有两个痛点。 首先是缺乏适当的错误处理。 但是,正确的错误处理可以被视为非功能性需求,因此我将在下一节使用测试驱动开发来解决这个问题。

第二点更微妙。 如果你看看GenerateQRCode,你可以看到我正在使用bytes.Buffer来满足png.Encode函数的签名。 或者,如果从另一个方向看它,我已经引入了这个缓冲区,因为GenerateQRCode想要返回一个字节切片。 该字节切片仅用于将图像写入文件。

这个重构步骤将摆脱缓冲区。 我将转换代码以使用满足io.Writer接口的任何内容,而不是传递字节切片,就像png.Encode函数一样。 我们来看看我的重构程序:

qrcoded.go
11 func main() {
12 	fmt.Println("Hello QR Code")
13 
14 	file, _ := os.Create("qrcode.png")
15 	defer file.Close()
16 
17 	GenerateQRCode(file, "555-2368")
18 }
19 
20 func GenerateQRCode(w io.Writer, code string) {
21 	img := image.NewNRGBA(image.Rect(0, 0, 21, 21))
22 	_ = png.Encode(w, img)
23 }

20 - 23

GenerateQRCode函数现在接受满足io.Writer接口的任何参数。 这是Go中常见的习惯用法,其中面向字节的输入和输出最好通过io.Reader和io.Writer完成。 使用writer作为第一个参数在标准库中也很常见。 编写器的使用使得函数变得更加简单 - 我正在管理缓冲区的代码行消失了。 相反,该功能现在只显示我想要显示的内容:创建图像并对其进行编码。

14 - 15

我现在显式创建了一个文件,并确保在main函数退出时关闭它,而不是使用ioutil.WriteFile函数。 目前,我还明确忽略了可能产生的错误; 下划线是后来改进的一个很好的标记。

17

其中,io.File满足io.Writer接口。 这意味着在这一行上我可以直接将文件传递给GenerateQRCode函数。 这完全消除了程序中字节切片和缓冲区的概念。

此示例显示了一点重构可以如何显着提高代码的可读性(和可理解性)。 正如您将在下一节中看到的,此更改还提高了代码的可测试性。 并且需要重复的是,这只是测试驱动开发的承诺:清晰,模块化,可测试的代码。

但是我们还没有庆祝。 就目前而言,这一变化打破了测试,他们甚至不再编译:

我重构后的影响
$ go test
# _/workspace/golang-tdd
./qrcoded_test.go:10: cannot use "555-2368" (type string) as type io.Writer in fu\
nction argument:
	string does not implement io.Writer (missing Write method)
./qrcoded_test.go:10: not enough arguments in call to GenerateQRCode
./qrcoded_test.go:10: GenerateQRCode("555-2368") used as value
./qrcoded_test.go:21: cannot use "555-2368" (type string) as type io.Writer in fu\
nction argument:
	string does not implement io.Writer (missing Write method)
./qrcoded_test.go:21: not enough arguments in call to GenerateQRCode
./qrcoded_test.go:21: GenerateQRCode("555-2368") used as value
FAIL	_/workspace/golang-tdd [build failed]

测试仍然假设GenerateQRCode接受一个参数并返回一个字节缓冲区。 幸运的是,测试驱动的开发再一次迫使我们采取小步骤,所以解决这个问题是微不足道的。

qrcoded_test.go
10 func TestGenerateQRCodeReturnsValue(t *testing.T) {
11 	buffer := new(bytes.Buffer)
12 	GenerateQRCode(buffer, "555-2368")
13 
14 	if buffer.Len() == 0 {
15 		t.Errorf("No QRCode generated")
16 	}
17 }
18 
19 func TestGenerateQRCodeGeneratesPNG(t *testing.T) {
20 	buffer := new(bytes.Buffer)
21 	GenerateQRCode(buffer, "555-2368")
22 	_, err := png.Decode(buffer)
23 
24 	if err != nil {
25 		t.Errorf("Generated QRCode is not a PNG: %s", err)
26 	}
27 }

在此之后,测试又回到绿色。 注意第一次测试如何变得更简单; 因为缓冲区不可能变为零,所以不再需要检查它。 尽管如此,即使测试发生了变化,它们仍然会测试完全相同的功能。

此示例还显示了测试驱动开发中的一个真正问题:过度使用。 许多重构不仅触及测试中的代码,还触及测试本身。 这意味着如果您有太多测试,那么您将花费更多时间来修复测试而不是实际改进代码。

事实上,如果你看一下当前的代码,你会看到有两个测试用于同一个路径。 因此,在测试相同数量的功能时,让我们减少测试量:

qrcoded_test.go
10 func TestGenerateQRCodeGeneratesPNG(t *testing.T) {
11 	buffer := new(bytes.Buffer)
12 	GenerateQRCode(buffer, "555-2368")
13 
14 	if buffer.Len() == 0 {
15 		t.Errorf("No QRCode generated")
16 	}
17 
18 	_, err := png.Decode(buffer)
19 
20 	if err != nil {
21 		t.Errorf("Generated QRCode is not a PNG: %s", err)
22 	}
23 }

此测试与前两个测试略有不同 - 它涵盖了完全相同的要求,但GenerateQRCode函数签名中的更改现在只需要更改一个测试。

作为一般规则,你应该尝试对每个流程进行一次单元测试。 如果您的受测单元提供多个流程,则值得考虑将你的单元拆分为多个单元。

当然,如果不运行go测试,测试驱动开发中的任何步骤都不会完成:

永远不要忘记进行测试 go test
$ go test 
PASS
ok  	_/workspace/golang-tdd		0.021s

这结束了本书中第一个真正的测试驱动开发周期。 我用了多个页面来解释我一直在做什么,但在现实生活中,这个循环最多需要5到10分钟,包括至少5次测试。

当然,只有运行单元测试足够快,这样的工作流程才可行。 我个人对“足够快”的定义大概是五秒钟。 go测试工具在合理的系统上有大约一秒的开销; 这给了我足够的空间来运行一百到两百个单元测试。 在本书中,我将展示几种技术,以尽可能缩短测试的运行时间。

测试错误处理

正如我在上一节所说,是时候提出新的要求了,实际上是一个非功能性的要求,那就是要有适当的错误处理。 对png.Decode的调用可能会导致错误,但GenerateQRCode不会对此做任何事情。

非测试驱动的方法是改变这样的函数:

qrcoded.go
20 func GenerateQRCode(w io.Writer, code string) error {
21 	img := image.NewNRGBA(image.Rect(0, 0, 21, 21))
22 	return png.Encode(w, img)
23 }

事实上,这正是我要做的,但是在我做出这个改变之前,测试驱动的开发需要一个失败的测试。 这并不像听起来那么微不足道。

每当png.Encode返回错误时,GenerateQRCode函数都应该返回错误。 换句话说,测试应该强制png.Encode返回错误,然后测试GenerateQRCode会导致错误。 让我们从查看png.Encode的文档开始:

  func Encode(w io.Writer, m image.Image) error  

Encode writes the Image m to w in PNG format. Any Image may be encoded, but images that are not image.NRGBA might be encoded lossily.

非常让人失望。文档清楚地表明png.Encode可以返回错误,但文档没有说明这是如何发生的。您当然可以跟踪源代码,但幸运的是Go标准库有一些约定:

  1. 可能发生的任何错误都会在该程序包的文档中进行命名和记录。
  2. 无效的参数通常会导致错误; 零参数通常会引起panic。
  3. 除非另有明确说明,否则将传递使用其中一个参数发生的任何错误。

让我们将这些标准与png.Encode进行比较:

  1. image/png的文档指定了两个错误,FormatError和UnsupportedError,但这些错误只能在解码过程中发生。换句话说,png.Encode本身不会返回错误。
  2. 在编译时将捕获创建无效接口,并且在这种情况下无法创建无效图像 - 对image.NewNRGBA的调用指定非空矩形,保证返回有效图像。 对于这两个参数,传递nil也不是一种选择,因为我们没有测试panic条件。
  3. image.Image类型不包含可能导致错误的相关方法,但是io.Writer界面确实如此。这可能是候选人。

因此,为了测试GenerateQRCode的错误流,我们需要一些实现io.Writer的东西,并且会在Write上导致错误。 一种选择是使用只读临时文件,但有一种更好的方法:使用测试替身(test double)。

测试替身是一种类型的简化实现,它表现出特定的行为以帮助测试。 传统上,有五种类型的测试替身:

傻瓜模式(Dummies)

完全没有任何行为的类型,只是因为被测单元的签名需要它们。

存根(Stubs)

实现最少行为以满足测试的类型。

嘲弄(Mocks)

部分实现,你可以定义对其方法的调用方式的期望。

间谍(Spies)

部分实现,你可以断言已调用特定方法。

假货(Fakes)

完整,轻量级的实现,例如内存数据库。

许多语言都有库,允许你在运行时创建存根,模拟和间谍。 但是,Go只允许您在编译时定义新类型,因此在Go中使用测试替身需要更多工作。 幸运的是,Go中接口的概念使它只是一点额外的工作。

如前所述,测试需要io.Writer的实现,它会在Write上生成错误。现在,定义一个简单的存根就足够了:

qrcoded_test.go
29 type ErrorWriter struct{}
30 
31 func (e *ErrorWriter) Write(b []byte) (int, error) {
32 	return 0, errors.New("Expected error")
33 }
34 
35 func TestGenerateQRCodePropagatesErrors(t *testing.T) {
36 	w := new(ErrorWriter)
37 	err := GenerateQRCode(w, "555-2368")
38 
39 	if err == nil || err.Error() != "Expected error" {
40 		t.Errorf("Error not propagated correctly, got %v", err)
41 	}
42 }

29

这一行定义了一个新类型,称为ErrorWriter。

31 - 33

此函数将ErrorWriter转换为满足io.Writer的存根。 它的实现很简单:每次调用Write都会返回错误。 这三条无害的线条是惯用的Go的典范。创建一个满足io.Writer的类型非常容易,这意味着我之前引入io.Writer参数的重构使得测试函数变得容易多了。

35 - 42

这些行定义了新的测试;,检查GenerateQRCode的结果并将其断言为预期的错误。

再次执行测试go test将再次失败。不是因为缺少功能,而是因为代码不再编译:

执行** go test**测试再次失败
$ go test
# _/workspace/golang-tdd
./qrcoded_test.go:37: GenerateQRCode(w, "555-2368") used as value
FAIL	_/Users/yigalduppen/src/golang-tdd [build failed]

现在测试假设GenerateQRCode返回错误,但它没有。所以让我们稍微改变GenerateQRCode函数:

qrcoded.go
20 func GenerateQRCode(w io.Writer, code string) error {
21 	img := image.NewNRGBA(image.Rect(0, 0, 21, 21))
22 	png.Encode(w, img)
23 	return nil
24 }

此更改足以使测试成功失败。

成功失败
$ go test
--- FAIL: TestGenerateQRCodePropagatesErrors (0.00 seconds)
	qrcoded_test.go:36: Error not propagated correctly, got <nil>
FAIL
exit status 1
FAIL	_/Users/yigalduppen/src/golang-tdd	0.020s

绿(Green)

在本节的介绍中,我已经放弃了解决方案:

qrcoded.go
20 func GenerateQRCode(w io.Writer, code string) error {
21 	img := image.NewNRGBA(image.Rect(0, 0, 21, 21))
22 	return png.Encode(w, img)
23 }

这种变化足以打成测试工作。

你可能想知道为什么我在更改GenerateQRCode的签名时没有立即执行此操作。 毕竟,是什么让png.Decode(…), 返回nil比返回png.Decode(…)好多了?

原因是Red步骤必须始终导致测试失败。 这不仅仅是因为教条,而它保证了你的新测试能够测试成功。

尽管测试驱动的开发需要很小的步骤,但仍然很容易出错。 如果在测试中出现这样的错误,你最终可能会在最不期望的情况下突然失败,完全没有明显的原因。这就是测试驱动开发决定测试失败的原因。

重构(Refactor)

正如我之前所说,重构不是可选的。但是,此时测试的代码量非常小,看起来结构合理。 所以在这种情况下我决定改进main中的错误处理。

qrcoded.go
11 func main() {
12 	log.Println("Hello QR Code")
13 
14 	file, err := os.Create("qrcode.png")
15 	if err != nil {
16 		log.Fatal(err)
17 	}
18 	defer file.Close()
19 
20 	err = GenerateQRCode(file, "555-2368")
21 	if err != nil {
22 		log.Fatal(err)
23 	}
24 }

现在使用日志包而不是fmt处理所有正常和错误输出。早期退出由log.Fatal处理,它自动调用os.Exit(1)。主要功能仍然没有单元测试,因此运行测试保持绿色。

是的,这真的是在欺骗:我只重构了未经测试的代码。相信我,当我说我只是出于教学目的而这样做时。

总结

在本章中,我介绍了go测试单元测试框架,并展示了如何使用它来实现测试驱动开发。

我已经解释过,重构是成功的测试驱动开发的先决条件,我已经展示了它如何确实可以改进你的代码。

此外,我已经展示了如何明智地使用小接口作为参数可以使您的Go代码更加可测试。

在下一章中,我将提供更多关于Red/Green/Refactor循环的示例,并提供一些关于如何创建干净,惯用且可测试的Go代码的示例。

创建QR码

测试驱动的开发就是一次只采取一小步。尽管如此,在采取下一步措施之前,仍然要有一个普遍的方向感。 在本章中,我将对QR代码进行高级概述,并使用测试驱动开发来概述总体方向。

究竟是什么QR码?

在前面的章节中,我已经展示了QR码的一些示例,你可能已经认识到它们。 基于这些示例,我可以向您显示任何可能的QR码,你仍然会将它们识别为QR码。 不幸的是,这并没有告诉我们如何生成QR码。

QR码是官方标准化的二维条码类型,ISO的优秀人员在一份名为“ISO / IEC 18004:2006(E)- QR码2005条码符号规范”的文件中解释了QR码的所有细节。”。 有了这样的头衔,会出现什么问题?

事实证明,有四种不同类型的QR码:它们被称为QR码模型1,QR码模型2,QR码2005和微QR码。 模型1已被弃用,所以本书不会打扰这些。 Model 2 QR码是QR Code 2005的严格子集,不需要任何特殊处理。 微代码非常有趣,但为了缩小范围,本书将重点介绍如何生成QR代码2005。

因此,要回答本节标题中的问题:QR码是由ISO/IEC 18002:2006(E)规定的暗方和浅方的二维条形码图案。

二维码术语

QR码是传送一些输入数据的一种方式:程序将输入数据编码为暗方形和亮方形的图案。 然后可以将该图案转换为图像并打印。 随后可以扫描打印的图像以获得原始输入数据。

黑暗和浅色方块称为模块 规范没有说明暗和正方形的确切颜色,但我经常将它们称为黑白模块。

QR码中的模块数量由其版本决定。 例如,版本1 QR码包含21x21模块; 最大的指定版本,版本40,包含177x177模块。

QR码的模式是三个不同子模式的组合:功能模式,对于每个版本总是相同的,并且用于校准扫描仪; 编码模式,包含您要传达的实际数据; 和数据屏蔽模式,它与编码模式相结合,以防止扫描仪混淆。

规范定义了更多的术语,但现在这已经足够了。

创建QR码

创建QR码遵循简单的8步程序。 期望最终用户指定输入数据,所需的纠错能力水平,然后程序执行以下操作:

  1. 数据分析-用于确定要编码的数据类型,然后定义可以使用的最小可能版本。

  2. 数据编码-将输入数据转换为8位代码字列表。

  3. 纠错-这会创建纠错码。

  4. 消息结构-这将数据和纠错码组合成可选填充的位列表。

  5. 模块放置-这会将消息转换为编码模式,并将其与功能模式相结合。

  6. 数据屏蔽-这适用于不同的数据屏蔽模式,并选择具有最佳结果的模式。

  7. 格式和版本信息-这会扩展模式的格式和版本信息,从而产生最终模式。

  8. 图像创建-将图案转换为实际图像。

这些步骤中的每一步都有其自身的挑战,但这就是你简要创建QR码的方法。

介绍版本

目前的实施距离实施8步计划还有很长的路要走。 但是测试驱动的开发完全取决于基础步骤,所以让我们从第一步开始:版本。 此时,版本的概念不存在。 因此,让我们创建一个包含这个概念的测试。

红(Red)

版本首先表明的是模式的大小。例如,版本1 QR码具有21x21模块的模式。为此创建测试听起来并不难:

qrcoded_test.go
31 func TestVersionDeterminesSize(t *testing.T) {
32 	buffer := new(bytes.Buffer)
33 	GenerateQRCode(buffer, "555-2368", Version(1))
34 
35 	img, _ := png.Decode(buffer)
36 	if width := img.Bounds().Dx(); width != 21 {
37 		t.Errorf("Version 1, expected 21 but got %d", width)
38 	}
39 }

33

在这里,我更改了GenerateQRCode的签名以获取其他版本参数。 请注意我没有说明版本的确切类型,只是当我说版本(1)时,我得到版本1的有效表示。

36-38

在这里,我检查所得图像的宽度确实是21像素宽。从技术上讲,图像的大小与图案的大小并不完全相关,但是,仅仅一小步骤。

我再次编写了一个测试,改变了GenerateQRCode的预期签名,因此go测试甚至无法正确编译。

将签名结果修复为以下代码:

qrcoded.go
26 func GenerateQRCode(w io.Writer, code string, version Version) error {
27 	img := image.NewNRGBA(image.Rect(0, 0, 21, 21))
28 	return png.Encode(w, img)
29 }
30 
31 type Version int8

31

在这里,我决定将版本作为顶级类型引入。它是int8的别名,因此可以通过强制转换整数来创建新版本:Version(24)。

26

新版本参数与新签名匹配。这当然会导致新的编译错误,因此旧的测试和主要功能也必须改变。 由于这些测试都不依赖于版本,因此可以使用任何评估版本的表达式。 我随意选择了版本(1)。

在修复所有对GenerateQRCode的调用之后,测试最终运行。通过。

正如我在前一章中所解释的那样,这是一个问题。 现在你可以明白为什么了。 我的测试目前涵盖’版本1 QR码是21x21模块大’的情况,但我在第27行生成的图像已经意外地匹配该情况11。 这表明为什么立即成功的测试实际上没有测试任何东西。

最简单的答案是创建一个测试另一个版本/大小组合的新测试。 但是有一种更好的方法,通常用于Go测试:它被称为表驱动测试。

表驱动测试都遵循相同的模式:首先,它们定义一个“表”,其中每一行描述特定情况的输入和预期输出。 然后测试迭代这些行,在输入上应用被测单元并将结果与预期输出进行比较:

qrcoded_test.go
40 func TestVersionDeterminesSize(t *testing.T) {
41 	table := []struct {
42 		version  int
43 		expected int
44 	}{
45 		{1, 21},
46 		{2, 25},
47 		{6, 41},
48 		{7, 45},
49 		{14, 73},
50 		{40, 177},
51 	}
52 
53 	for _, test := range table {
54 		buffer := new(bytes.Buffer)
55 		GenerateQRCode(buffer, "555-2368", Version(test.version))
56 		img, _ := png.Decode(buffer)
57 		if width := img.Bounds().Dx(); width != test.expected {
58 			t.Errorf("Version %2d, expected %3d but got %3d",
59 				test.version, test.expected, width)
60 		}
61 	}
62 }

41-51

这个多行语句创建表; 它是一个初始化表达式,其中type是一个匿名结构。 每个结构包含两个字段:版本和预期大小。 第45-50行定义了许多测试用例。我可以枚举所有可能的版本和大小,但为了节省空间12我使用了一个样本。 请注意我是如何明确包含边界值1和40的。

53-61

这个块遍历表,执行我之前为每一行创建的测试。

58-59

表驱动测试可以涵盖很多情况,因此如果您的错误消息尽可能具体,则可以让你的生活更轻松。

这次去测试终于失败了。

绿(Green)

表驱动测试的一个好处是它们允许您在输入和输出上看到aditional结构。 在这种情况下,很明显模块的数量可以由函数f(x)= 4x + 17给出。这使得初始解决方案非常简单:

qrcoded.go
26 func GenerateQRCode(w io.Writer, code string, version Version) error {
27 	size := 4*int(version) + 17
28 	img := image.NewNRGBA(image.Rect(0, 0, size, size))
29 	return png.Encode(w, img)
30 }

这足以使测试运行,但并不完全令人满意……

重构(Refactor)

TestVersionDeterminesSize测试函数涵盖整个GenerateQRCode函数; 但是,顾名思义它只是实际检查尺寸计算。感觉好像测试太多了,这通常表明被测单元也做得太多了。

为了集中测试,第一步是将大小计算提取到一个单独的函数中:

提取大小计算
func DeriveSizeFromVersion(version Version) int {
	return 4*int(version) + 17
}

这个函数的名称已经放弃了下一步:为什么不把它作为Version类型的方法呢?

qrcoded.go
26 func GenerateQRCode(w io.Writer, code string, version Version) error {
27 	size := version.PatternSize()
28 	img := image.NewNRGBA(image.Rect(0, 0, size, size))
29 	return png.Encode(w, img)
30 }
31 
32 type Version int8
33 
34 func (v Version) PatternSize() int {
35 	return 4*int(v) + 17
36 }

34

Go允许您将方法添加到当前包中定义的任何类型,即使它是基本类型的别名。 我已经将方法命名为PatternSize而不是Size来阐明此函数的意图。

27

我现在已经删除了显式大小计算,并直接调用version.PatternSize()。

这是一个使用go测试来验证一切仍然可以正常工作的好时机。 注意代码如何变得更好:GenerateQRCode函数不再需要知道Version可以转换为int - 它只是询问版本的大小。

现在我已经从GenerateQRCode函数中提取了大小计算,因此可以集中测试:

qrcoded_test.go
53 for _, test := range table {
54 	size := Version(test.version).PatternSize()
55 	if size != test.expected {
56 		t.Errorf("Version %2d, expected %3d but got %3d",
57 			test.version, test.expected, size)
58 	}
59 }

此测试不再引用GenerateQRCode,它不了解图像,也不了解PNG。相反,它正如其名称所暗示的那样:测试您可以从函数中确定模式大小。

您可能会注意到,我们新近关注的测试不再测试GenerateQRCode实际上是否与版本交互以确定图像大小。 这是正确的,原因有两个:首先,图像大小不仅来自版本; 它还来自每个模块的像素数,以及所谓的静区的大小,这是模式周围正式定义的填充。 换句话说,检查图像大小是错误的。

第二个原因更加个人化:在我的观点中,测试发生这种微不足道的相互作用是不合理的。 测试驱动开发的每个支持者都不同意这种观点,因此您应该自己决定自己喜欢什么。

如果我要测试这种交互,我就是这样做的。 首先,我将把Version变成一个接口,所以我可以在我的测试中创建一个MockVersion。 然后,此MockVersion将包含其他方法来检查是否实际调用了Size方法。 总而言之,很多代码都是为了进行简单的交互。

总结

在本章中,我向您展示了QR码世界的高级概述。 我已经使用这个概述来介绍版本的领域概念,我已经向您介绍了表驱动测试,这是Go中常见的习惯用法。

我已经展示了测试驱动开发如何帮助您识别过多的代码,以及重构步骤如何帮助您分离关注点。

在下一章中,我将展示如何创建包可以帮助您更多地实现模块化,可测试的代码。

  1. qrcoded代表QR码守护进程; 这是用于在后台运行的程序的Unix命名约定。
  2. 如果你想完全欣赏代码示例,你真的应该去Tour of Go。 它可以在http://tour.golang.org找到。
  3. 一个着名的数字,我经常想在奇怪的调试会话期间调用。
  4. t.Errorf的效果不同于其他语言中的典型断言和期望。 即使调用了t.Errorf,Go测试仍将继续执行。这允许您一次查看所有测试失败。其他语言中的许多测试框架建议您每个测试用例只应使用一个断言来实现相同的目标; Go中没有。
  5. 注意测试如何在没有任何明确的编译步骤的情况下获取更改-当您运行go test时,它会自动重新编译您的代码,就像go run一样。
  6. 至少不是在测试驱动的开发中。如果忽略重构步骤,那么您正在练习测试优先开发。这也是一种可行的开发策略,但它需要前期设计。
  7. 每个PNG文件都以字节0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A开头。 如果没有,则不是PNG。
  8. 这当然引出了一个问题:你应该写多少次测试? 现在,答案是:足够了。在后面的章节中,我将介绍测试覆盖率的概念,这将有助于量化“过度”和“恰到好处”的概念。
  9. 扫描和解码QR码不在本书中。
  10. 请注意规范如何防止我们混淆。
  11. 不可否认,经过精心准备的事故。
  12. 而且因为我很懒。

原书地址:https://leanpub.com/golang-tdd/read

其实读完后,还是对测试驱动开发有了一定的了解,但是感觉是不是可以写得更通俗易懂些。

终于发现,在学习技术的过程中,有一些技术类的书是怎么来的啦,边看,边翻译。或者连翻译作者自己都没有弄懂就开始翻译了。哎,所以, 要读经典,要读原著是有原因的。

历时3天,将这个《go 测试驱动开发》书翻译完成。在翻译的过程中,主要使用了google翻译,而且将书中的例子编译证明。

当然,这个电子书没有完成,不知道原作者是否会更新,如果不更新我自己都有想法写一份。