为什么你写的代码总是有 Bug?用它来保证 Go 代码质量( 四 )

为了调用注册的路由,Application 类型实现 http.Handler 接口 。http.Handler 作为 Application 的内嵌结构体字段,因此 Application 可以调用 http.Handler 接口实现的ServeHTTP 函数
清单 8req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/list/%d/item", test.ListID), nil)if err != nil {   t.Errorf("error creating request: %v", err)}w := httptest.NewRecorder()a.ServeHTTP(w, req)回顾一下代码清单 5,构造 Application 是为了在测试中使用 。ServeHTTP 函数需要两个参数:http.ResponseWriter 和 http.Request 。http.NewRequest 构造 http.Request,httptest.NewRecorder 构造 http.ResponseRecorder——即 http.Response。
http.NewRecorder 函数的返回 ResponseRecorder 值实现了 ResponseWriter 接口 。调用路由请求后,ResponseRecorder 可以用来分析了 。其中最关键的字段 Code 和 Body,前者是该请求的实际响应码,后者是一个指向响应内容的 bytes.Buffer 类型的指针 。

译者注:这里的 http.ResponseWriter 和 http.Request 实现了 Golang 中常见的 Writer和 Reader 接口,即 输出 和 输入,在 http 请求中即 Response 和 Request 。
清单 9if want, got := http.StatusOK, w.Code; want != got {    t.Errorf("expected status code: %v, got status code: %v", want, got)}清单 9 中,实际的响应码和预期的响应码做对比 。如果不同,将调用 t.Errorf,它将输出失败原因 。
清单 10var items []item.Itemresp := web.Response{    Results: items,}if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {    t.Errorf("error decoding response body: %v", err)}if d := cmp.Diff(expectedItems, items); d != "" {    t.Errorf("unexpected difference in response body:n%v", d)}示例中使用自定义响应体 web.Response,使用 键为 results 的 JSON 字符串存储路由返回信息 。代码清单 10 中声明了一个 []item.Item 类型的变量 items,用于和预期值对比 。初始化 items 变量传递给 resp 的字段 results 。接下来,items 会随着解析路由响应体数据到 resp 中,从而包含响应体的数据 。
google 的 go-cmp[4] 包可替代 reflect.DeepEqual,在对比 struct,map,slice 和 array 时更安全,更易用 。调用 cmp.Diff 对比清单 6 中定义的种子数据和实际响应体中返回的数据,如果不等,测试将失败,并且将差异输出到标准输出(stdout)中 。
测试技巧就测试而言,最好的建议是尽早测试,并且经常测试,而不是将测试放到开发之后考虑,而且测试应该推动、驱动应用程序的开发 。这就是“测试驱动开发(TDD)” 。通常情况下,没有随时测试代码 。在编写代码时,将测试的想法抛到脑后,自己(开发人员)默认编写的代码是可测试的 。代码单元(通常是一个函数)不管再小都能进行测试 。你的服务进行越多测试,未知的就越少,隐藏的副作用(bug)就越少 。
有了下面这些技巧,你的测试将洞察力,更易读,更快 。
表测试表测试是一种编写测试的方式,可以防止针对同一代码单元的不同可测试结果重复测试断言 。以下面的求和函数为例:
清单 11// Add takes an indefinite amount of operands and adds them together, returning// the sum of the operation.func Add(operands ...int) int {    var sum int    for _, operand := range operands {        sum += operand    }    return sum}在测试中,我想确保函数可以处理以下情况:
  • 没有参数(operands),应返回 0 。
  • 一个参数,直接返回参数值 。
  • 两个参数,返回这两个数之和 。
  • 三个参数,则返回这三个数之和 。
彼此独立地编写这些测试将导致重复许多相同的调用和断言 。我认为,更好的方法是利用表测试 。为了编写表测试,必须定义一片匿名声明的结构,其中包含我们每个测试用例的元数据 。然后可以使用循环遍历不同测试用例的这些条目,并可以对用例进行测试和独立运行 t.Run 。t.Run 需要两个参数,子测试函数和这个子测试函数的函数名,子测试函数必须符合这种类型:func(*testing.T) 。


推荐阅读