Go语言:利用 TDD 逐步为一个字典应用创建完整的 CRUD API
Posted slowlydance2me
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go语言:利用 TDD 逐步为一个字典应用创建完整的 CRUD API相关的知识,希望对你有一定的参考价值。
前言
键
存储值,并快速查找它们。键
视为单词,将值视为定义。
所以,难道还有比构建我们自己的字典更好的学习 map 的方式吗?
正文
首先编写测试
在 dictionary_test.go
中编写代码:
package main import "testing" func TestSearch(t *testing.T) dictionary := map[string]string"test": "this is just a test" got := Search(dictionary, "test") want := "this is just a test" if got != want t.Errorf("got \'%s\' want \'%s\' given, \'%s\'", got, want, "test")
map
关键字开头,需要两种类型。第一个是键的类型,写在 []
中。第二个是值的类型,跟在 []
之后。尝试运行测试
运行 go test
后编译器会提示失败信息 ./dictionary_test.go:8:9: undefined: Search
。
编写最少量的代码让测试运行并检查输出
在 dictionary.go
中:
package main func Search(dictionary map[string]string, word string) string return ""
dictionary_test.go:12: got \'\' want \'this is just a test\' given, \'test\'
编写足够的代码使测试通过
func Search(dictionary map[string]string, word string) string return dictionary[word]
从 map 中获取值和数组相同,都是通过 map[key]
的方式。
重构
func TestSearch(t *testing.T) dictionary := map[string]string"test": "this is just a test" got := Search(dictionary, "test") want := "this is just a test" assertStrings(t, got, want) func assertStrings(t *testing.T, got, want string) t.Helper() if got != want t.Errorf("got \'%s\' want \'%s\'", got, want)
创建一个 assertStrings
辅助函数并删除 given
的部分让实现更通用。
使用自定义的类型
我们可以通过为 map 创建新的类型并使用 Search
方法改进字典的使用。
在 dictionary_test.go
中:
func TestSearch(t *testing.T) dictionary := Dictionary"test": "this is just a test" got := dictionary.Search("test") want := "this is just a test" assertStrings(t, got, want)
Dictionary
类型了,但是我们还没有定义它。然后要在 Dictionary
实例上调用 Search
方法。assertStrings
。
dictionary.go
中:
type Dictionary map[string]string func (d Dictionary) Search(word string) string return d[word]
在这里,我们创建了一个 Dictionary
类型,它是对 map
的简单封装。定义了自定义类型后,我们可以创建 Search
方法。
首先编写测试
func TestSearch(t *testing.T) dictionary := Dictionary"test": "this is just a test" t.Run("known word", func(t *testing.T) got, _ := dictionary.Search("test") want := "this is just a test" assertStrings(t, got, want) ) t.Run("unknown word", func(t *testing.T) _, err := dictionary.Search("unknown") want := "could not find the word you were looking for" if err == nil t.Fatal("expected to get an error.") assertStrings(t, err.Error(), want) )
在 Go 中处理这种情况的方法是返回第二个参数,它是一个 Error
类型。
Error
类型可以使用 .Error()
方法转换为字符串,我们将其传递给断言时会执行此操作。
我们也用 if
来保护 assertStrings
,以确保我们不在 nil
上调用 .Error()
。
尝试运行测试
这不会通过编译
./dictionary_test.go:18:10: assignment mismatch: 2 variables but 1 values
编写最少量的代码让测试运行并检查输出
func (d Dictionary) Search(word string) (string, error)
return d[word], nil
现在你的测试将会失败,并显示更加清晰的错误信息
dictionary_test.go:22: expected to get an error.
编写足够的代码使测试通过
func (d Dictionary) Search(word string) (string, error) definition, ok := d[word] if !ok return "", errors.New("could not find the word you were looking for") return definition, nil
为了使测试通过,我们使用了一个 map 查找的有趣特性。
它可以返回两个值。第二个值是一个布尔值,表示是否成功找到 key
。
此特性允许我们区分单词不存在还是未定义。
重构
var ErrNotFound = errors.New("could not find the word you were looking for") func (d Dictionary) Search(word string) (string, error) definition, ok := d[word] if !ok return "", ErrNotFound return definition, nil
我们通过将错误提取为变量的方式,摆脱 Search
中魔术错误(magic error)。这也会使我们获得更好的测试。
t.Run("unknown word", func(t *testing.T) _, got := dictionary.Search("unknown") assertError(t, got, ErrNotFound) ) func assertError(t *testing.T, got, want error) t.Helper() if got != want t.Errorf("got error \'%s\' want \'%s\'", got, want)
通过创建一个新的辅助函数,我们能够简化测试,并使用 ErrNotFound
变量,如果我们将来更改显示错误的文字,测试也不会失败。
首先编写测试
我们现在有很好的方法来搜索字典。但是,我们无法在字典中添加新单词。
func TestAdd(t *testing.T) dictionary := Dictionary dictionary.Add("test", "this is just a test") want := "this is just a test" got, err := dictionary.Search("test") if err != nil t.Fatal("should find added word:", err) if want != got t.Errorf("got \'%s\' want \'%s\'", got, want)
在这个测试中,我们利用 Search
方法使字典的验证更加容易。
编写最少量的代码让测试运行并检查输出
在 dictionary.go
中:
func (d Dictionary) Add(word, definition string)
测试现在应该会失败:
dictionary_test.go:31: should find added word: could not find the word you were looking for
编写足够的代码使测试通过
func (d Dictionary) Add(word, definition string) d[word] = definition
向 map 添加元素也类似于数组。你只需指定键并给它赋一个值。
引用类型
Map 有一个有趣的特性,不使用指针传递你就可以修改它们。
这是因为 map
是引用类型。这意味着它拥有对底层数据结构的引用,就像指针一样。
它底层的数据结构是 hash table
或 hash map
Map 作为引用类型是非常好的,因为无论 map 有多大,都只会有一个副本
引用类型引入了 maps
可以是 nil
值。如果你尝试使用一个 nil
的 map,你会得到一个 nil 指针异常
,这将导致程序终止运行。
由于 nil 指针异常
,你永远不应该初始化一个空的 map 变量:
var m map[string]string
相反,你可以像我们上面那样初始化空 map,或使用 make
关键字创建 map
dictionary = map[string]string // OR dictionary = make(map[string]string)
这两种方法都可以创建一个空的 hash map
并指向 dictionary
。这确保永远不会获得 nil 指针异常
重构
在我们的实现中没有太多可以重构的地方,但测试可以简化一点。
func TestAdd(t *testing.T) dictionary := Dictionary word := "test" definition := "this is just a test" dictionary.Add(word, definition) assertDefinition(t, dictionary, word, definition) func assertDefinition(t *testing.T, dictionary Dictionary, word, definition string) t.Helper() got, err := dictionary.Search(word) if err != nil t.Fatal("should find added word:", err) if definition != got t.Errorf("got \'%s\' want \'%s\'", got, definition)
我们为单词和定义创建了变量,并将定义断言移到了自己的辅助函数中。
我们的 Add
看起来不错。除此之外,我们没有考虑当我们尝试添加的值已经存在时会发生什么!
如果值已存在,map 不会抛出错误。相反,它们将继续并使用新提供的值覆盖该值。
这在实践中很方便,但会导致我们的函数名称不准确。Add
不应修改现有值。它应该只在我们的字典中添加新单词。
首先编写测试
func TestAdd(t *testing.T) t.Run("new word", func(t *testing.T) dictionary := Dictionary word := "test" definition := "this is just a test" err := dictionary.Add(word, definition) assertError(t, err, nil) assertDefinition(t, dictionary, word, definition) ) t.Run("existing word", func(t *testing.T) word := "test" definition := "this is just a test" dictionary := Dictionaryword: definition err := dictionary.Add(word, "new test") assertError(t, err, ErrWordExists) assertDefinition(t, dictionary, word, definition) )
对于此测试,我们修改了 Add
以返回错误,我们将针对新的错误变量 ErrWordExists
进行验证。我们还修改了之前的测试以检查是否为 nil
错误。
尝试运行测试
编译将失败,因为我们没有为 Add
返回值。
./dictionary_test.go:30:13: dictionary.Add(word, definition) used as value
./dictionary_test.go:41:13: dictionary.Add(word, "new test") used as value
编写最少量的代码让测试运行并检查输出
在 dictionary.go
中:
var ( ErrNotFound = errors.New("could not find the word you were looking for") ErrWordExists = errors.New("cannot add word because it already exists") ) func (d Dictionary) Add(word, definition string) error d[word] = definition return nil
现在我们又得到两个错误。我们仍在修改值,并返回 nil
错误。
dictionary_test.go:43: got error \'%!s(<nil>)\' want \'cannot add word because it already exists\'
dictionary_test.go:44: got \'new test\' want \'this is just a test\'
编写足够的代码使测试通过
func (d Dictionary) Add(word, definition string) error _, err := d.Search(word) switch err case ErrNotFound: d[word] = definition case nil: return ErrWordExists default: return err return nil
这里我们使用 switch
语句来匹配错误。如上使用 switch
提供了额外的安全,以防 Search
返回错误而不是 ErrNotFound
。
重构
我们没有太多需要重构的地方,但随着对错误使用的增多,我们还可以做一些修改。
const ( ErrNotFound = DictionaryErr("could not find the word you were looking for") ErrWordExists = DictionaryErr("cannot add word because it already exists") ) type DictionaryErr string func (e DictionaryErr) Error() string return string(e)
我们将错误声明为常量,这需要我们创建自己的 DictionaryErr
类型来实现 error
接口。
你可以在 Dave Cheney 的这篇优秀文章中了解更多相关的细节。
简而言之,它使错误更具可重用性和不可变性
首先编写测试
func TestUpdate(t *testing.T) word := "test" definition := "this is just a test" dictionary := Dictionaryword: definition newDefinition := "new definition" dictionary.Update(word, newDefinition) assertDefinition(t, dictionary, word, newDefinition)
以上是关于Go语言:利用 TDD 逐步为一个字典应用创建完整的 CRUD API的主要内容,如果未能解决你的问题,请参考以下文章
Go 语言:如何利用好 TDD 学习指针并了解 Golang 中的 error 处理