Skip to content

测试驱动开发

原文链接

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

测试驱动开发

从在测试中捕获这些需求开始。

  1. 编写一个测试

  2. 让编译通过

  3. 运行测试,查看失败原因并检查错误消息是很有意义的

  4. 编写足够的代码以使测试通过

  5. 重构

以 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 testmain.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
}