这两天在学习 Golang 如何 TDD ,了解到匿名结构体切片在 TableDrivenTests 中经常用到。
Writing good tests is not trivial, but in many situations a lot of ground can be covered with table-driven tests: Each table entry is a complete test case with inputs and expected results, and sometimes with additional information such as a test name to make the test output easily readable. If you ever find yourself using copy and paste when writing a test, think about whether refactoring into a table-driven test or pulling the copied code out into a helper function might be a better option.
大意就是 table driven 的每个数据用例定义了测试的输入输出,把测试数据和测试逻辑分开,易于维护和保持代码整洁。而 table driven 的数据结构就是一个匿名结构体切片 (anonymous struct slice) 。为了在系统了解它,在网上搜了几篇典型的文章并总结如下。
结构体基础
结构体 (struct) 将多个不同类型的字段集中组成一种复合类型,按声明时的字段顺序初始化。
type user struct { name string age byte } user := user {"Tom", 2}
typevarvar
// 在函数外部定义匿名结构体并赋值给 configvar config struct { APIKey string OAuthConfig oauth.Config }// 定义并初始化并赋值给 datadata := struct { Title string Users []*User }{ title, users }
匿名结构体使用场景
匿名结构体在四种常见情景下的用法。
组织全局变量
属于同一类的全局变量可通过匿名结构体组织在一起。
var config struct { APIKey string OAuthConfig oauth.Config } config.APIKey = "BADC0C0A"
数据模版
可在后端把数据组织成前端需要的格式传给渲染模版
package mainimport ( "html/template" "net/http" "strings")type Paste struct { Expiration string Content []byte UUID string}func pasteHandler(w http.ResponseWriter, r *http.Request) { paste_id := strings.TrimPrefix(r.URL.Path, "/paste") paste := &Paste{UUID: paste_id} keep_alive := false burn_after_reading := false data := struct { Paste *Paste KeepAlive bool BurnAfterReading bool } { paste, keep_alive, burn_after_reading, } t, _ := template.ParseFiles("templates/paste.html") t.Execute(w, data) }
dataPaste
Expiration: {{ .Paste.Expiration }} UUID: {{ .Paste.UUID}} {{ if .BurnAfterReading }} BurnAfterReading: True{{ else }} BurnAfterReading: False{{ end }}
测试案例数据
在写测试代码时,经常用到匿名结构体生成用例的输入输出,为了覆盖各个测试维度,通常结合切片使用,构成了测试样例尽可能地覆盖所有可能发生情况。
var indexRuneTests = []struct { s string rune rune out int}{ {"a A x", 'A', 2}, {"some_text=some_value", '=', 9}, {"a", 'a', 3}, {"ab", '', 4}, }
嵌入式锁 (Embedded lock)
var hits struct { sync.Mutex n int} hits.Lock() hits.n++ hits.Unlock()
Golang TDD 技巧
结合例子分析:请求 Github 的接口,获取某个项目的最新版本号。因为请求 Github 接口涉及到系统外部的响应,在写测试时把请求外部系统的逻辑抽象放在一个实现了 Golang interface 的方法中。在测试中,我们写测试代码时实现 Golang Interface 定义的这个请求外部系统的方法,这样我们就能模拟外部系统的返回而不是依赖外部系统的返回。
package mainimport ( "encoding/json" "fmt" "io/ioutil" "net/http" "os")type ReleasesInfo struct { Id uint `json:"id"` TagName string `json:"tag_name"` } type ReleaseInfoer interface { GetLatestReleaseTag(string) (string, error) } type GithubReleaseInfoer struct{}// Function to actually query the Github API for the release information.func (gh GithubReleaseInfoer) GetLatestReleaseTag(repo string) (string, error) { apiUrl := fmt.Sprintf("https://api.github.com/repos/%s/releases", repo) response, err := http.Get(apiUrl) if err != nil { return "", err } defer response.Body.Close() body, err := ioutil.ReadAll(response.Body) if err != nil { return "", err } releases := []ReleasesInfo{} if err := json.Unmarshal(body, &releases); err != nil { return "", err } tag := releases[0].TagName return tag, nil }// Function to get the message to display to the end userfunc getReleaseTagMessage(ri ReleaseInfoer, repo string) (string, error) { tag, err := ri.GetLatestReleaseTag(repo) if err != nil { return "", fmt.Errorf("Error querying Github API: %s", err) } return fmt.Sprintf("The latest release is %s", tag), nil } func main() { gh := GithubReleaseInfoer{} msg, err := getReleaseTagMessage(gh, "docker/machine") if err != nil { fmt.Fprintln(os.Stderr, msg) } fmt.Println(msg) }
ReleasesInfoReleaseInfoer
package mainimport ( "errors" "reflect" "testing")type FakeReleaseInfoer struct { Tag string Err error } func (f FakeReleaseInfoer) GetLatestReleaseTag(repo string) (string, error) { if f.Err != nil { return "", f.Err } return f.Tag, nil } func TestGetReleaseTagMessage(t *testing.T) { cases := []struct { f FakeReleaseInfoer repo string expectedMsg string expectedErr error }{ { f: FakeReleaseInfoer{ Tag: "v0.1.0", Err: nil, }, repo: "doesnt/matter", expectedMsg: "The latest release is v0.1.0", expectedErr: nil, }, { f: FakeReleaseInfoer{ Tag: "v0.1.0", Err: errors.New("TCP timeout"), }, repo: "doesnt/foo", expectedMsg: "", expectedErr: errors.New("Error querying Github API: TCP timeout"), }, } for _, c := range cases { msg, err := getReleaseTagMessage(c.f, c.repo) if !reflect.DeepEqual(err, c.expectedErr) { t.Errorf("Expected err to be %q but it was %q", c.expectedErr, err) } if c.expectedMsg != msg { t.Errorf("Expected %q but got %q", c.expectedMsg, msg) } } }
FakeReleaseInfoerReleaseInfoerGetLatestReleaseTagTestGetReleaseTagMessage
datagetReleaseTagMessage
Run:
go test -v === RUN TestGetReleaseTagMessage --- PASS: TestGetReleaseTagMessage (0.00s) PASS ok github.com/wenweih/testing 0.023s