Hello,World

未匹配的标注

Hello, World

查看本章的所有代码

按照传统,我们用新语言编写的第一个程序都是 Hello,world。

按照以下路径创建目录 $GOPATH/src/github.com/{your-user-id}/hello.

我们假定你使用的是基于 Unix 的系统,你的名字是「bob」并且愿意遵循 Go 关于 $GOPATH 的约定(这是最简单的设置方式)。那么你可以执行以下命令 mkdir -p $GOPATH/src/github.com/bob/hello 快速创建目录。

对于后面的章节,存放代码的文件夹名称可以按照自己的喜好定义,例如下一节可以用 $GOPATH/src/github.com/{your-user-id}/integers。一些读者喜欢将所有代码放到一个文件夹中,比如「learn-go-with-tests/hello」。简而言之,这取决于你如何组织你的文件夹。

在该目录下创建名为 hello.go 的文件并编写以下代码,在终端输入 go run hello.go 来运行程序。

package main

import "fmt"

func main() {

    fmt.Println("Hello, world")

}

它是如何运行的

用 Go 编写程序,你需要定义一个 main 包,并在其中定义一个 main 函数。包是一种将相关的 Go 代码组合到一起的方式。

func 关键字通过函数名和函数体来定义函数。

通过 import "fmt" 导入一个包含 Println 函数的包,我们用它来打印输出。

如何测试

你打算如何测试这个程序?最好处理方式是将你的辅助代码从(domain)代码中移动到外面来 (辅助作用). fmt.Println 就是一个辅助作用的函数 (打印输出) 而我们要发送的字符串是我们的重点所在(domain)。

所以为了更容易测试,我们把这些问题拆开

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 %q want %q", got, want)
    }
}

在解释之前,让我们来运行代码。在终端运行 go test,它应该会通过测试!为了再次验证,可以尝试改变 want 字符串来破坏测试的结果。

注意,你不必在多个测试框架之间进行选择,然后再去理解如何安装它们。你需要的一切都内建在 Go 语言中,语法与你将要编写的其余代码相同。

编写测试

编写测试和函数很类似,其中有一些规则

  • 程序需要编写在 xxx_test.go 文件中

  • 测试函数的命名必须以单词 Test 开始

  • 测试函数只接受一个参数 t *testing.T

现在这些信息足以让我们明白,类型为 *testing.T 的变量 t 是你在测试框架中的 「hook」,所以当你想让测试失败时可以执行 t.Fail() 之类的操作。

我们讨论一些新话题:

if

Go 的 if 语句与其他编程语言非常类似。

声明变量

我们使用 varName := value 的语法声明变量,它允许我们在测试中复用一些值,从而使代码更具可读性。

t.Errorf

我们调用 tErrorf 方法 打印一条消息并使测试失败。f 表示格式化,它允许我们构建一个字符串,并将值插入占位符值 %q 中。当你的测试失败时,它能够让你清楚是什么错误导致的。

您可以在fmt go doc中阅读关于占位符字符串的更多信息。对于测试,%q 非常有用,因为它将值括在双引号中。

后面我们将探讨方法和函数之间的区别。

Go 文档

Go 的另一个高质量特性是文档。你可以通过运行 godoc -http :8000 来启动本地文档。如果你访问 localhost:8000/pkg,你将看到系统上安装的所有包。

绝大多数标准库都有优秀的文档和示例。浏览 localhost:8000/pkg/testing/ 是非常值得的,去看一下对你有价值的内容吧!

如果你没有 godoc 命令,那么你可能正在使用新版本的 Go(1.13或更高版本),它不在包括 godoc。你可以通过 go get golang.org/x/tools/cmd/godoc,来手动安装它。

Hello, YOU

现在有了测试,我们可以安全地对我们的软件进行迭代了。

在上一个示例中,我们在写好代码 之后 编写了测试,以便让你学会如何编写测试和声明函数。从此刻起,我们将 先编写测试

我们的下一个需求是对指定的人发起问候。

让我们从在测试中捕获这些需求开始。这是基本的测试驱动开发,可以确保我们的测试用例 真正 在测试我们想要的功能。当你回顾你编写的测试时,可能存在即使代码不能正常工作,你的测试也可能继续通过的风险。

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 ()

当使用像 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 englishHelloPrefix =  "Hello, "

现在我们来重构代码

const englishHelloPrefix = "Hello, "

func Hello(name string) string {
    return englishHelloPrefix + name
}

重构之后,重新测试以确保程序无误。

常量应该可以提高应用程序的性能,因为它可以在每次调用 Hello 时节省创建 Hello 字符串实例的时间。

显然,对于这个例子来说,性能提升是微不足道的!但是创建常量的价值是可以快速理解值的含义,有时还可以帮助提高性能。

再次回到 Hello, world

下一个需求是当我们的函数用空字符串调用时,它默认为打印 「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)
        }
    })
}

这里我们将介绍测试库中的另一个工具——子测试。有时,对一个「事情」进行分组测试,然后再对不同场景进行子测试非常有效。

这种方法的一个好处是,您可以编写在其他测试中也可以使用的共享代码。

当我们检查信息是否符合预期时,会有重复的代码。

重构不 是针对生产代码!

重要的是,你的测试 清楚地说明 了代码需要做什么。

我们可以并且应该重构我们的测试

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)
    })

}

我们在这里做了什么?

我们将断言重构为一个函数。 这减少了重复并提高了测试的可读性。在 Go 中,你可以在其他函数中声明函数并将它们赋值给变量。你可以像调用普通函数一样调用它们。我们需要传入 t *testing.T,这样我们就可以在需要的时候令测试代码失败。

t.Helper() 需要告诉测试套件这个方法是辅助函数。通过这样做,当测试失败时所报告的行号将在 函数调用中 而不是在辅助函数内部。这将帮助其他开发人员更容易地跟踪问题。如果你仍然不理解,请注释掉它,使测试失败并观察测试输出。

现在我们有了一个很好的失败测试,让我们使用 if 修复代码。

const englishHelloPrefix = "Hello, "

func Hello(name string) string {
    if name == "" {
        name = "World"
    }
    return englishHelloPrefix + 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 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 language == "Spanish" {
        return "Hola, " + name
    }

    return englishHelloPrefix + name
}

测试现在应该通过了。

现在是 重构 的时候了。您应该看到代码中的一些问题,其中有一些重复的 「魔术」字符串。自己尝试重构它,每次更改都要重新运行测试,以确保重构不会破坏任何内容。

const spanish = "Spanish"
const englishHelloPrefix = "Hello, "
const spanishHelloPrefix = "Hola, "

func Hello(name string, language string) string {
    if name == "" {
        name = "World"
    }

    if language == spanish {
        return spanishHelloPrefix + name
    }

    return englishHelloPrefix + 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 englishHelloPrefix + name
}

switch

当你有很多 if 语句检查一个特定的值时,通常使用 switch 语句来代替。如果我们希望稍后添加更多的语言支持,我们可以使用 switch 来重构代码,使代码更易于阅读和扩展。

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
}

编写一个测试,现在包括你可以选择不同的语言来问候,你应该看到扩展这个 惊艳 的功能是多么的简单。

最后一次重构?

你可能会抱怨说也许我们的函数正在变得很臃肿。对此最简单的重构是将一些功能提取到另一个函数中。

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
}

一些新概念:

  • 在我们的函数签名中,我们使用了 命名返回值 (prefix string)

  • 这将在你的函数中创建一个名为 prefix 的变量。

    • 他将被分配「零」值。这取决于类型,例如 int 是 0,对于字符串来说它是 ""

      • 你可以只通过调用 return 而不是 return prefix 来返回它设置值。
    • 这将在 Go Doc 中显示你的函数,这样它可以使你的代码更清晰。

  • 如果没有其他 case 语句匹配,将会执行 default 分支。

  • 函数名称以小写字母开头。在 Go 中,公共函数以大写字母开始,私有函数以小写字母开头。我们不希望我们算法的内部结构暴露给外部,所以我们将这个功能私有化。

总结

谁会知道你可以从 Hello, world 中学到这么多东西呢?

现在你应该对一下的内容有了一定的理解:

Go 的一些语法

  • 编写测试

  • 用参数和返回类型声明函数

  • if, constswitch

  • 声明变量和常量

TDD 过程和这些步骤 为什么 是重要的

  • 编写一个失败的测试,并查看失败信息,我们知道现在有一个为需求编写的 相关 的测试,并且看到它产生了 易于理解的失败描述

  • 编写最少量的代码使其通过,以获得可以运行的程序

  • 然后 重构,基于我们测试的安全性,以确保我们拥有易于使用的精心编写的代码

在我们的例子中,我们通过小巧易懂的步骤从 Hello()Hello("name"),到 Hello("name", "french")

与「现实世界」的软件相比,这当然是微不足道的,但原则依然通用。TDD 是一门需要通过开发去实践的技能,通过将问题分解成更小的可测试的组件,你编写软件将会更加轻松。

原文地址 Learn Go with Tests

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
贡献者:1
讨论数量: 0
发起讨论 查看所有版本


暂无话题~