通过编写测试来学习 Go 你好,世界

2025-05-28

通过编写测试来学习 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")
}
Enter fullscreen mode Exit fullscreen mode

工作原理

当你用 Go 编写程序时,你会main定义一个包含函数的包mainfunc关键字就是你如何定义一个包含名称和函数主体的函数。

我们import "fmt"正在导入一个包含Println用于打印的函数的包。

如何测试

你该如何测试呢?最好将你的“域”代码与外界(副作用)隔离开来。这fmt.Println是一个副作用(打印到标准输出),我们发送的字符串就是我们的域。

因此,让我们将这些问题分开,以便更容易测试

package main

import "fmt"

func Hello() string {
    return "Hello, world"
}

func main() {
    fmt.Println(Hello())
}
Enter fullscreen mode Exit fullscreen mode

我们再次使用 创建了一个新函数,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)
    }
}
Enter fullscreen mode Exit fullscreen mode

在解释之前,我们先运行一下代码。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)
    }
}
Enter fullscreen mode Exit fullscreen mode

现在运行go test,你应该有一个编译错误

./hello_test.go:6:18: too many arguments in call to Hello
    have (string)
    want ()
Enter fullscreen mode Exit fullscreen mode

当使用像 Go 这样的静态类型语言时,监听编译器的指令非常重要。编译器知道你的代码应该如何组合在一起并工作,所以你不需要自己去理解。

在这种情况下,编译器会告诉你需要做什么才能继续。我们必须修改函数Hello以接受参数。

编辑Hello函数以接受字符串类型的参数

func Hello(name string) string {
    return "Hello, world"
}
Enter fullscreen mode Exit fullscreen mode

如果您尝试再次运行测试,main.go则会编译失败,因为您没有传递参数。发送“world”即可使其通过。

func main() {
    fmt.Println(Hello("world"))
}
Enter fullscreen mode Exit fullscreen mode

现在,当你运行测试时,你应该会看到类似

hello_test.go:10: got 'Hello, world' want 'Hello, Chris''
Enter fullscreen mode Exit fullscreen mode

我们终于有了一个编译程序,但根据测试它并不符合我们的要求。

让我们使用 name 参数来使测试通过,并将其与Hello,

func Hello(name string) string {
    return "Hello, " + name
}
Enter fullscreen mode Exit fullscreen mode

现在运行测试应该就通过了。通常,作为 TDD 周期的一部分,我们现在应该进行重构

这里没有太多需要重构的地方,但我们可以引入另一个语言特性常量

常量

常量定义如下

const helloPrefix = "Hello, "
Enter fullscreen mode Exit fullscreen mode

我们现在可以重构我们的代码

const helloPrefix = "Hello, "

func Hello(name string) string {
    return helloPrefix + name
}
Enter fullscreen mode Exit fullscreen mode

重构后,重新运行测试以确保没有破坏任何东西。

常量应该可以提高应用程序的性能,因为它可以节省"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)
        }
    })

}
Enter fullscreen mode Exit fullscreen mode

这里我们介绍测试工具库中的另一个工具——子测试。有时,围绕某个“事物”对测试进行分组,然后设置描述不同场景的子测试会很有用。

这种方法的好处是您可以设置可在其他测试中使用的共享代码。

当我们检查消息是否是我们期望的时,会出现重复的代码。

重构不仅仅针对生产代码!

重要的是,你的测试要明确说明代码需要做什么。

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

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

}
Enter fullscreen mode Exit fullscreen mode

我们在这里做了什么?

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

t.Helper()需要告诉测试套件此方法是辅助方法。这样做,当测试失败时,报告的行号将出现在函数调用中,而不是测试辅助方法中。这将帮助其他开发人员更轻松地追踪问题。如果您仍然不明白,请将其注释掉,然后执行测试失败并观察测试输出。

现在我们有了一个编写良好的失败测试,​​让我们修复代码。

const helloPrefix = "Hello, "

func Hello(name string) string {
    if name == "" {
        name = "World"
    }
    return helloPrefix + name
}
Enter fullscreen mode Exit fullscreen mode

如果我们运行测试,我们应该看到它满足新的要求,并且我们没有意外破坏其他功能

纪律

让我们再回顾一下这个循环

  • 编写测试
  • 使编译器通过
  • 运行测试,查看是否失败,并检查错误消息是否有意义
  • 编写足够的代码以使测试通过
  • 重构

从表面上看,这可能看起来很乏味,但坚持反馈循环很重要。

它不仅可以确保您拥有相关的测试,还可以通过重构测试的安全性来帮助确保您设计出优秀的软件。

看到测试失败是一项重要的检查,因为它还能让你看到错误消息的样子。作为一名开发人员,如果失败的测试无法清晰地说明问题所在,那么在代码库中工作会非常困难。

通过确保测试速​​度快并设置工具以便运行测试简单,您可以在编写代码时进入流动状态。

通过不编写测试,您就必须通过运行软件来手动检查代码,这会破坏您的流程状态,并且您不会节省任何时间,尤其是从长远来看。

继续!更多要求

天哪,我们还有更多需求。现在我们需要支持第二个参数,指定问候语的语言。如果传入的语言我们无法识别,就默认为英语。

我们应该有信心使用 TDD 轻松充实这一功能!

为西班牙语合格用户编写一个测试。将其添加到现有套件中。

    t.Run("in Spanish", func(t *testing.T) {
        got := Hello("Elodie", "Spanish")
        want := "Hola, Elodie"
        assertCorrectMessage(t, got, want)
    })
Enter fullscreen mode Exit fullscreen mode

记住不要作弊!先测试一下。当你尝试运行测试时,编译器应该会报错,因为你调用了Hello两个参数,而不是一个。

./hello_test.go:27:19: too many arguments in call to Hello
    have (string, string)
    want (string)
Enter fullscreen mode Exit fullscreen mode

通过添加另一个字符串参数来解决编译问题Hello

func Hello(name string, language string) string {
    if name == "" {
        name = "World"
    }
    return helloPrefix + name
}
Enter fullscreen mode Exit fullscreen mode

当您尝试再次运行测试时,它会抱怨没有传递足够的参数到Hello您的其他测试中,并且main.go

./hello.go:15:19: not enough arguments in call to Hello
    have (string)
    want (string, string)
Enter fullscreen mode Exit fullscreen mode

通过传递空字符串来修复它们。现在,除了我们的新场景之外,所有测试都应该可以编译并通过。

hello_test.go:29: got 'Hola, Elodie' want 'Hello, Elodie'
Enter fullscreen mode Exit fullscreen mode

我们可以if在这里检查语言是否等于“西班牙语”,如果是,则更改消息

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

    if language == "Spanish" {
        return "Hola, " + name
    }

    return helloPrefix + name
}
Enter fullscreen mode Exit fullscreen mode

测试现在应该可以通过了。

现在是时候重构了。你应该会在代码中看到一些问题,比如一些“魔法”字符串,其中一些是重复的。尝试自己重构一下,每次修改后都要重新运行测试,确保重构没有破坏任何东西。

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
}
Enter fullscreen mode Exit fullscreen mode

法语

  • 编写一个测试,断言如果你通过了,"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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

现在编写一个测试,包含您选择的语言的问候语,您应该会看到扩展我们惊人的功能是多么简单。

最后一次...重构?

你可能会说我们的函数可能有点大了。最简单的重构方法是将一些功能提取到另一个函数中,而且你已经知道如何声明函数了。

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
}
Enter fullscreen mode Exit fullscreen mode

一些新概念:

  • 在我们的函数签名中,我们已经创建了一个命名的返回值 (prefix string)
  • prefix这将在您的函数中 创建一个变量
    • 它将被赋值为“零”。这取决于类型,例如,对于ints 为 0,而对于字符串则为""
      • return您只需调用而不是即可返回其设置的任何内容return prefix
    • 这将显示在您函数的 Go Doc 中,以便使您的代码意图更清晰。
  • defaultcase如果其他语句都不匹配,则将分支到 switch case 中
  • 函数名以小写字母开头。在 Go 语言中,公共函数以大写字母开头,私有函数以小写字母开头。我们不希望算法的内部实现暴露给外界,因此我们将此函数设为私有。

总结

谁知道您可以从中获得这么多Hello, world

现在你应该对

Go 的一些语法

  • 编写测试
  • 声明函数,包含参​​数和返回类型
  • if,,elseswitch
  • 声明变量和常量

了解 TDD 流程以及这些步骤的重要性

  • 编写一个失败的测试并观察它是否失败,这样我们就知道我们已经为我们的需求编写了一个相关的测试,并且看到它产生了一个易于理解的失败描述
  • 编写最少的代码使其通过,这样我们就知道我们的软件可以运行
  • 然后进行重构,并通过测试的安全性来确保我们拥有精心编写且易于使用的代码

在我们的案例中,我们通过小而易于理解的步骤从Hello()变为。Hello("name")Hello("name", "french")

当然,与“现实世界”的软件相比,这微不足道,但其原则依然适用。TDD 是一项需要实践才能掌握的技能,但如果能够将问题分解成可测试的更小组件,编写软件就会轻松得多。

文章来源:https://dev.to/quii/learn-go-by-writing-tests--m63
PREV
你可能不知道的 6 种有用的前端技术
NEXT
在 Kubernetes 上部署应用程序:完整指南!