测试驱动开发¶
Hello World 示例¶
hello.go
package main
import "fmt"
func Hello() string {
return "Hello, world"
}
func main() {
fmt.Println(Hello())
}
go run hello.go # hello world
go bulid hello.go # 编译生成二进制文件
ls # hello hello.go
./hello # Hello world
测试 Hello World 示例¶
为了更容易测试,我们把问题拆分开。
将你「领域」内的代码和外界(会引起副作用)分离开会更好。
fmt.Println
会产生副作用(打印到标准输出),我们发送的字符串在自己的领域内。
package main
import "fmt"
func Hello() string {
return "Hello, world"
}
func main() {
fmt.Println(Hello())
}
这次我们在定义中添加了另一个关键字 string
。这意味着这个函数将返回一个字符串。
现在创建一个 hello_test.go
的新文件,来为 Hello
函数编写测试。
package main
import "testing"
func TestHello(t *testing.T) {
go := Hello()
want := "Hello, world"
if got != want {
t.Errorf("got '%q' want '%q'", got, want)
}
}
运行测试
go test
编写测试程序的规则¶
编写测试和函数很类似,其中有一些规则
- 程序需要在一个名为
xxx_test.go
的文件中编写 - 测试函数的命名必须以单词
Test
开始 - 测试函数只接受一个参数
t *testing.T
类型为 *testing.T
的变量 t
是你在测试框架中的 hook(钩子),所以当你想让测试失败时可以执行 t.Fail()
之类的操作。
t.Errorf
的用法:我们调用 t
的 Errorf
方法打印一条消息并使测试失败。f
表示格式化,它允许我们构建一个字符串,并将值插入占位符值 %q
中。当你的测试失败时,它能够让你清楚是什么错误导致的。
测试驱动开发¶
从在测试中捕获这些需求开始。
-
编写一个测试
-
让编译通过
-
运行测试,查看失败原因并检查错误消息是很有意义的
-
编写足够的代码以使测试通过
-
重构
以 Hello World 为例。
下一个需求是指定问候的接受者。
先写测试程序
package main
import "testing"
func TestHello(t *testing.T) {
got := Hello("Chris")
want := "Hello, Chris"
if got != want {
t.Errorf("got '%q' want '%q'", got, want)
}
}
这时运行 go test
将得到如下错误
./hello_test.go:6:18: too many arguments in call to Hello
have (string)
want ()
在这种情况下,编译器告诉你需要怎么做才能继续。我们必须修改函数 Hello
来接受一个参数。
修改 Hello
函数以接受字符串类型的参数
func Hello(name string) string {
return "Hello, world"
}
尝试再次运行测试 go test
,main.go
会编译失败,因为没有传递参数。
传入参数「world」让它通过。
func main() {
fmt.Println(Hello("world"))
}
再次运行测试 go test
,将得到如下错误
hello_test.go:10: got 'Hello, world' want 'Hello, Chris''
为了使测试通过,我们使用 name
参数并用 Hello,
字符串连接它
func Hello(name string) string {
return "Hello, " + name
}
现在再运行测试就通过了。通常作为 TDD 周期的一部分,我们该着手 重构 了。
开始重构代码。
常量可以提高应用程序的性能,它避免了每次使用 Hello
时创建 "Hello, "
字符串实例。
const englishHelloPrefix = "Hello, "
func Hello(name string) string {
return englishHelloPrefix + name
}
重构之后,重新测试以确保程序无误。
下一个需求是当我们的函数用空字符串调用时,它默认为打印 "Hello, World" 而不是 "Hello,"
首先编写一个新的失败测试
func TestHello(t *testing.T) {
t.Run("saying hello to people", func(t *testing.T) {
got := Hello("Chris")
want := "Hello, Chris"
if got != want {
t.Errorf("got '%q' want '%q'", got, want)
}
})
t.Run("say hello world when an empty string is supplied", func(t *testing.T) {
got := Hello("")
want := "Hello, World"
if got != want {
t.Errorf("got '%q' want '%q'", got, want)
}
})
}
重构测试代码。
将断言重构为函数。这减少了重复并且提高了测试的可读性。在函数中声明函数并将它们分配给变量,可以像调用普通函数一样调用它们。t.Helper()
需要告诉测试套件这个方法是辅助函数(helper)。通过这样做,当测试失败时所报告的行号将在函数调用中而不是在辅助函数内部。
func TestHello(t *testing.T) {
assertCorrectMessage := func(t *testing.T, got, want string) {
t.Helper()
if got != want {
t.Errorf("got '%q' want '%q'", got, want)
}
}
t.Run("saying hello to people", func(t *testing.T) {
got := Hello("Chris")
want := "Hello, Chris"
assertCorrectMessage(t, got, want)
})
t.Run("empty string defaults to 'world'", func(t *testing.T) {
got := Hello("")
want := "Hello, World"
assertCorrectMessage(t, got, want)
})
}
改写业务代码。
const englishHelloPrefix = "Hello, "
func Hello(name string) string {
if name == "" {
name = "World"
}
return englishHelloPrefix + name
}
如果我们运行测试,应该看到它满足了新的要求。
满足更多需求¶
这个内容复习不看也行。反正跟前面差不多了。
现在需要支持第二个参数,指定问候的语言。如果一种不能识别的语言被传进来,就默认为英语。
为使用西班牙语的用户编写测试,将其添加到现有的测试用例中。
t.Run("in Spanish", func(t *testing.T) {
got := Hello("Elodie", "Spanish")
want := "Hola, Elodie"
assertCorrectMessage(t, got, want)
})
运行测试,编译器会报错,因为用了两个参数而不是一个来调用 Hello
。
./hello_test.go:27:19: too many arguments in call to Hello
have (string, string)
want (string)
通过向 Hello
添加另一个字符串参数来解决编译问题。
func Hello(name string, language string) string {
if "" == name {
name = "World"
}
return englishHelloPrefix + name;
}
再次运行测试时,它会报错在其他测试和 hello.go
中没有传递足够的参数给 Hello
函数
./hello.go:15:19: not enough arguments in call to Hello
have (string)
want (string, string)
通过传递空字符串来解决它们。现在,除了新场景外,所有测试都应该编译并通过
hello_test.go:29: got 'Hello, Elodie' want 'Hola, Elodie'
使用 if
检查语言是否是「西班牙语」,如果是就修改信息
func Hello(name string, language string) string {
if "" == name {
name = "World"
}
if "Spanish" == language {
return "Hola, " + name
}
return englishHelloPrefix + name
}
测试通过。
重构代码。
const spanish = "Spanish"
const helloPrefix = "Hello, "
const spanishHelloPrefix = "Hola, "
func Hello(name string, language string) string {
if "" = name {
name = "World"
}
if language == spanish {
return spanishHelloPrefix + name
}
return engglishHelloPrefix + name
}
再加个法语吧。
直接上业务代码
TDD 真是太麻烦了 ( x
悄悄不写测试。问就是忘了写。
func Hello(name string, language string) string {
if name == "" {
name = "World"
}
prefix := englishHelloPrefix
switch language {
case french:
prefix = frenchHelloPrefix
case spanish:
prefix = spanishHelloPrefix
}
return prefix + name
}
重构它。提取一个 greetingPrefix 函数出来。
func Hello(name string, language string) string {
if name == "" {
name = "World"
}
return greetingPrefix(language) + name
}
func greetingPrefix(language string) (prefix string) {
switch language {
case french:
prefix = frenchHelloPrefix
case spanish:
prefix = spanishHelloPrefix
default:
prefix = englishHelloPrefix
}
return
}