通过编写测试来学习 Go
你好世界
这篇文章是名为learn-go-with-tests的 WIP 项目中的第一篇。
来自 README
- 通过编写测试来探索 Go 语言
- 掌握 TDD 基础知识。Go 是学习 TDD 的好语言,因为它简单易学,并且内置了测试功能。
- 相信你能够开始用 Go 编写健壮、经过良好测试的系统
假设您已经安装并设置了 Go,并且具有一些编程的初级知识。
你好世界
按照惯例,你用一门新语言编写的第一个程序应该是“Hello, world”。创建一个名为的文件hello.go
并写入以下代码。运行它,请输入go run hello.go
。
package main
import "fmt"
func main() {
fmt.Println("Hello, world")
}
工作原理
当你用 Go 编写程序时,你会main
定义一个包含函数的包main
。func
关键字就是你如何定义一个包含名称和函数主体的函数。
我们import "fmt"
正在导入一个包含Println
用于打印的函数的包。
如何测试
你该如何测试呢?最好将你的“域”代码与外界(副作用)隔离开来。这fmt.Println
是一个副作用(打印到标准输出),我们发送的字符串就是我们的域。
因此,让我们将这些问题分开,以便更容易测试
package main
import "fmt"
func Hello() string {
return "Hello, world"
}
func main() {
fmt.Println(Hello())
}
我们再次使用 创建了一个新函数,func
但这次我们string
在定义中添加了另一个关键字。这意味着这个函数返回一个string
。
现在创建一个名为的新文件,hello_test.go
我们将在其中为我们的Hello
函数编写测试
package main
import "testing"
func TestHello(t *testing.T) {
got := Hello()
want := "Hello, world"
if got != want {
t.Errorf("got '%s' want '%s'", got, want)
}
}
在解释之前,我们先运行一下代码。go test
在终端里运行一下。应该已经通过了!为了检查一下,可以尝试故意修改字符串来破坏测试want
。
请注意,您无需在多个测试框架之间进行选择,也无需解读测试 DSL 来编写测试。您所需的一切都内置于该语言中,并且语法与您将要编写的其他代码相同。
编写测试
编写测试就像编写函数一样,只需遵循一些规则
- 它需要放在一个类似以下名称的文件中
xxx_test.go
- 测试函数必须以单词开头
Test
- 测试函数仅接受一个参数
t *testing.T
现在,只要知道你的t
类型*testing.T
是你进入测试框架的“钩子”就足够了,这样你就可以t.Fail()
在你想要失败的时候做一些事情。
新事物
if
Go 中的 If 语句与其他编程语言非常相似。
声明变量
我们使用语法声明一些变量varName := value
,这让我们可以在测试中重复使用一些值以提高可读性
t.Errorf
我们正在调用上的Errorf
方法t
,它将打印一条消息并导致测试失败。F
代表格式,它允许我们构建一个字符串,并将值插入到占位符值中%s
。当你让测试失败时,你应该清楚它的工作原理。
我们稍后将探讨方法和函数之间的区别。
转到文档
Go 的另一个优质功能是文档。您可以通过运行 来本地启动文档godoc -http :8000
。如果您访问localhost:8000/pkg,您将看到系统上安装的所有软件包。
标准库的绝大部分都有出色的文档和示例。访问http://localhost:8000/pkg/testing/可以查看其中有哪些内容。
你好,你
现在我们有了测试,我们可以安全地迭代我们的软件。
在上一个示例中,我们在代码编写完成后才编写测试,以便您可以了解如何编写测试和声明函数。从现在开始,我们将先编写测试。
我们的下一个要求是让我们指定问候的接收者。
首先,让我们在测试中捕捉这些需求。这是基本的测试驱动开发,可以确保测试确实测试了我们想要的内容。当你回顾性地编写测试时,即使代码没有按预期运行,测试也可能继续通过,这是有风险的。
package main
import "testing"
func TestHello(t *testing.T) {
got := Hello("Chris")
want := "Hello, Chris"
if got != want {
t.Errorf("got '%s' want '%s'", got, want)
}
}
现在运行go test
,你应该有一个编译错误
./hello_test.go:6:18: too many arguments in call to Hello
have (string)
want ()
当使用像 Go 这样的静态类型语言时,监听编译器的指令非常重要。编译器知道你的代码应该如何组合在一起并工作,所以你不需要自己去理解。
在这种情况下,编译器会告诉你需要做什么才能继续。我们必须修改函数Hello
以接受参数。
编辑Hello
函数以接受字符串类型的参数
func Hello(name string) string {
return "Hello, world"
}
如果您尝试再次运行测试,main.go
则会编译失败,因为您没有传递参数。发送“world”即可使其通过。
func main() {
fmt.Println(Hello("world"))
}
现在,当你运行测试时,你应该会看到类似
hello_test.go:10: got 'Hello, world' want 'Hello, Chris''
我们终于有了一个编译程序,但根据测试它并不符合我们的要求。
让我们使用 name 参数来使测试通过,并将其与Hello,
func Hello(name string) string {
return "Hello, " + name
}
现在运行测试应该就通过了。通常,作为 TDD 周期的一部分,我们现在应该进行重构。
这里没有太多需要重构的地方,但我们可以引入另一个语言特性常量
常量
常量定义如下
const helloPrefix = "Hello, "
我们现在可以重构我们的代码
const helloPrefix = "Hello, "
func Hello(name string) string {
return helloPrefix + name
}
重构后,重新运行测试以确保没有破坏任何东西。
常量应该可以提高应用程序的性能,因为它可以节省"Hello, "
每次Hello
调用时创建字符串实例的时间。
需要明确的是,在这个例子中,性能提升微不足道!但值得考虑创建常量来捕捉值的含义,有时还可以提高性能。
你好,世界……再次
下一个要求是,当我们的函数使用空字符串调用时,它默认打印“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 '%s' want '%s'", 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 '%s' want '%s'", got, want)
}
})
}
这里我们介绍测试工具库中的另一个工具——子测试。有时,围绕某个“事物”对测试进行分组,然后设置描述不同场景的子测试会很有用。
这种方法的好处是您可以设置可在其他测试中使用的共享代码。
当我们检查消息是否是我们期望的时,会出现重复的代码。
重构不仅仅针对生产代码!
重要的是,你的测试要明确说明代码需要做什么。
我们可以而且应该重构我们的测试。
func TestHello(t *testing.T) {
assertCorrectMessage := func(t *testing.T, got, want string) {
t.Helper()
if got != want {
t.Errorf("got '%s' want '%s'", 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)
})
}
我们在这里做了什么?
我们将断言重构为一个函数。这减少了重复代码,并提高了测试的可读性。在 Go 语言中,你可以在其他函数中声明函数,并将其赋值给变量。然后,你可以像调用普通函数一样调用它们。我们需要传入一个参数t *testing.T
,以便在需要时让测试代码失败。
t.Helper()
需要告诉测试套件此方法是辅助方法。这样做,当测试失败时,报告的行号将出现在函数调用中,而不是测试辅助方法中。这将帮助其他开发人员更轻松地追踪问题。如果您仍然不明白,请将其注释掉,然后执行测试失败并观察测试输出。
现在我们有了一个编写良好的失败测试,让我们修复代码。
const helloPrefix = "Hello, "
func Hello(name string) string {
if name == "" {
name = "World"
}
return helloPrefix + name
}
如果我们运行测试,我们应该看到它满足新的要求,并且我们没有意外破坏其他功能
纪律
让我们再回顾一下这个循环
- 编写测试
- 使编译器通过
- 运行测试,查看是否失败,并检查错误消息是否有意义
- 编写足够的代码以使测试通过
- 重构
从表面上看,这可能看起来很乏味,但坚持反馈循环很重要。
它不仅可以确保您拥有相关的测试,还可以通过重构测试的安全性来帮助确保您设计出优秀的软件。
看到测试失败是一项重要的检查,因为它还能让你看到错误消息的样子。作为一名开发人员,如果失败的测试无法清晰地说明问题所在,那么在代码库中工作会非常困难。
通过确保测试速度快并设置工具以便运行测试简单,您可以在编写代码时进入流动状态。
通过不编写测试,您就必须通过运行软件来手动检查代码,这会破坏您的流程状态,并且您不会节省任何时间,尤其是从长远来看。
继续!更多要求
天哪,我们还有更多需求。现在我们需要支持第二个参数,指定问候语的语言。如果传入的语言我们无法识别,就默认为英语。
我们应该有信心使用 TDD 轻松充实这一功能!
为西班牙语合格用户编写一个测试。将其添加到现有套件中。
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 helloPrefix + name
}
当您尝试再次运行测试时,它会抱怨没有传递足够的参数到Hello
您的其他测试中,并且main.go
./hello.go:15:19: not enough arguments in call to Hello
have (string)
want (string, string)
通过传递空字符串来修复它们。现在,除了我们的新场景之外,所有测试都应该可以编译并通过。
hello_test.go:29: got 'Hola, Elodie' want 'Hello, Elodie'
我们可以if
在这里检查语言是否等于“西班牙语”,如果是,则更改消息
func Hello(name string, language string) string {
if name == "" {
name = "World"
}
if language == "Spanish" {
return "Hola, " + name
}
return helloPrefix + 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 helloPrefix + name
}
法语
- 编写一个测试,断言如果你通过了,
"French"
你就会得到"Bonjour, "
- 看到它失败了,检查错误消息是否易于阅读
- 对代码进行最小的合理更改
你可能写过类似这样的内容
func Hello(name string, language string) string {
if name == "" {
name = "World"
}
if language == spanish {
return spanishHelloPrefix + name
}
if language == french {
return frenchHelloPrefix + name
}
return helloPrefix + name
}
switch
当你有大量if
语句检查特定值时,通常使用switch
语句来代替。如果我们以后希望添加更多语言支持,可以使用它switch
来重构代码,使其更易于阅读和扩展。
func Hello(name string, language string) string {
if name == "" {
name = "World"
}
prefix := helloPrefix
switch language {
case french:
prefix = frenchHelloPrefix
case spanish:
prefix = spanishHelloPrefix
}
return prefix + name
}
现在编写一个测试,包含您选择的语言的问候语,您应该会看到扩展我们惊人的功能是多么简单。
最后一次...重构?
你可能会说我们的函数可能有点大了。最简单的重构方法是将一些功能提取到另一个函数中,而且你已经知道如何声明函数了。
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 = englishPrefix
}
return
}
一些新概念:
- 在我们的函数签名中,我们已经创建了一个命名的返回值
(prefix string)
。 prefix
这将在您的函数中 创建一个变量- 它将被赋值为“零”。这取决于类型,例如,对于
int
s 为 0,而对于字符串则为""
return
您只需调用而不是即可返回其设置的任何内容return prefix
。
- 这将显示在您函数的 Go Doc 中,以便使您的代码意图更清晰。
- 它将被赋值为“零”。这取决于类型,例如,对于
default
case
如果其他语句都不匹配,则将分支到 switch case 中- 函数名以小写字母开头。在 Go 语言中,公共函数以大写字母开头,私有函数以小写字母开头。我们不希望算法的内部实现暴露给外界,因此我们将此函数设为私有。
总结
谁知道您可以从中获得这么多Hello, world
?
现在你应该对
Go 的一些语法
- 编写测试
- 声明函数,包含参数和返回类型
if
,,else
switch
- 声明变量和常量
了解 TDD 流程以及这些步骤的重要性
- 编写一个失败的测试并观察它是否失败,这样我们就知道我们已经为我们的需求编写了一个相关的测试,并且看到它产生了一个易于理解的失败描述
- 编写最少的代码使其通过,这样我们就知道我们的软件可以运行
- 然后进行重构,并通过测试的安全性来确保我们拥有精心编写且易于使用的代码
在我们的案例中,我们通过小而易于理解的步骤从Hello()
变为。Hello("name")
Hello("name", "french")
当然,与“现实世界”的软件相比,这微不足道,但其原则依然适用。TDD 是一项需要实践才能掌握的技能,但如果能够将问题分解成可测试的更小组件,编写软件就会轻松得多。
文章来源:https://dev.to/quii/learn-go-by-writing-tests--m63