51Testing软件测试论坛

标题: Go中的高级单元测试模式(上) [打印本页]

作者: lsekfe    时间: 2022-3-4 10:06
标题: Go中的高级单元测试模式(上)
一个好的开发者总是测试他们的代码,然而,普通的测试方法在某些情况下可能太简单了。根据项目的复杂程度,你可能需要运行高级测试来准确评估代码的性能。
  在这篇文章中,我们将研究Go中的一些测试模式,这将帮助你为任何项目编写有效的测试。我们将介绍嘲弄、测试夹具、测试助手和黄金文件等概念,你将看到如何在实际场景中应用每种技术。
  要跟上这篇文章,你应该有Go中单元测试的知识。让我们开始吧!
  测试HTTP处理程序
  首先,让我们考虑一个常见的场景,测试HTTP处理程序。HTTP处理程序应该与它们的依赖关系松散地结合在一起,这样就可以很容易地隔离一个元素进行测试而不影响代码的其他部分。如果你的HTTP处理程序最初设计得很好,测试应该是相当简单的。
  检查状态代码
  让我们考虑一个基本的测试,检查以下HTTP处理程序的状态代码。
  1.  func index(w http.ResponseWriter, r *http.Request) {
  2.       w.WriteHeader(http.StatusOK)
  3.   }
复制代码
上面的index() 处理程序应该为每个请求返回一个200 OK的响应。让我们用下面的测试来验证该处理程序的响应。
  1. func TestIndexHandler(t *testing.T) {
  2.       w := httptest.NewRecorder()
  3.       r := httptest.NewRequest(http.MethodGet, "/", nil)
  4.       index(w, r)
  5.       if w.Code != http.StatusOK {
  6.           t.Errorf("Expected status: %d, but got: %d", http.StatusOK, w.Code)
  7.       }
  8.   }
复制代码
 在上面的代码片段中,我们使用httptest 包来测试index() 处理器。我们返回了一个httptest.ResponseRecorder ,它通过NewRecorder() 方法实现了http.ResponseWriter 接口。http.ResponseWriter 记录了任何突变,使我们可以在测试中进行断言。
  我们还可以使用httptest.NewRequest() 方法创建一个HTTP请求。这样做可以指定处理程序所期望的请求类型,如请求方法、查询参数和响应体。在通过http.Header 类型获得http.Request 对象后,你还可以设置请求头。
  在用http.Request 对象和响应记录器调用index() 处理程序后,你可以使用Code 属性直接检查处理程序的响应。要对响应的其他属性进行断言,比如头或正文,你可以访问响应记录器上的适当方法或属性。
  1.  $ go test -v
  2.   === RUN   TestIndexHandler
  3.   --- PASS: TestIndexHandler (0.00s)
  4.   PASS
  5.   ok      github.com/ayoisaiah/random 0.004s
复制代码
外部依赖性
  现在,让我们考虑另一种常见的情况,即我们的HTTP处理器对外部服务有依赖性。
  1. func getJoke(w http.ResponseWriter, r *http.Request) {
  2.       u, err := url.Parse(r.URL.String())
  3.       if err != nil {
  4.           http.Error(w, err.Error(), http.StatusInternalServerError)
  5.           return
  6.       }
  7.       jokeId := u.Query().Get("id")
  8.       if jokeId == "" {
  9.           http.Error(w, "Joke ID cannot be empty", http.StatusBadRequest)
  10.           return
  11.       }
  12.       endpoint := "https://icanhazdadjoke.com/j/" + jokeId
  13.       client := http.Client{
  14.           Timeout: 10 * time.Second,
  15.       }
  16.       req, err := http.NewRequest(http.MethodGet, endpoint, nil)
  17.       if err != nil {
  18.           http.Error(w, err.Error(), http.StatusInternalServerError)
  19.           return
  20.       }
  21.       req.Header.Set("Accept", "text/plain")
  22.       resp, err := client.Do(req)
  23.       if err != nil {
  24.           http.Error(w, err.Error(), http.StatusInternalServerError)
  25.           return
  26.       }
  27.       defer resp.Body.Close()
  28.       b, err := ioutil.ReadAll(resp.Body)
  29.       if err != nil {
  30.           http.Error(w, err.Error(), http.StatusInternalServerError)
  31.           return
  32.       }
  33.       if resp.StatusCode != http.StatusOK {
  34.           http.Error(w, string(b), resp.StatusCode)
  35.           return
  36.       }
  37.       w.Header().Set("Content-Type", "text/plain")
  38.       w.WriteHeader(http.StatusOK)
  39.       w.Write(b)
  40.   }
  41.   func main() {
  42.       mux := http.NewServeMux()
复制代码
在上面的代码块中,getJoke 处理程序期望有一个id 查询参数,它用来从Random dad joke API中获取一个笑话。
  让我们为这个处理程序写一个测试。
  1.  func TestGetJokeHandler(t *testing.T) {
  2.       table := []struct {
  3.           id         string
  4.           statusCode int
  5.           body       string
  6.       }{
  7.           {"R7UfaahVfFd", 200, "My dog used to chase people on a bike a lot. It got so bad I had to take his bike away."},
  8.           {"173782", 404, `Joke with id "173782" not found`},
  9.           {"", 400, "Joke ID cannot be empty"},
  10.       }
  11.       for _, v := range table {
  12.           t.Run(v.id, func(t *testing.T) {
  13.               w := httptest.NewRecorder()
  14.               r := httptest.NewRequest(http.MethodGet, "/joke?id="+v.id, nil)
  15.               getJoke(w, r)
  16.               if w.Code != v.statusCode {
  17.                   t.Fatalf("Expected status code: %d, but got: %d", v.statusCode, w.Code)
  18.               }
  19.               body := strings.TrimSpace(w.Body.String())
  20.               if body != v.body {
  21.                   t.Fatalf("Expected body to be: '%s', but got: '%s'", v.body, body)
  22.               }
  23.           })
  24.       }
  25.   }
复制代码
 我们使用表格驱动测试来测试处理程序对一系列输入的影响。第一个输入是一个有效的Joke ID ,应该返回一个200 OK的响应。第二个是一个无效的ID,应该返回一个404响应。最后一个输入是一个空的ID,应该返回一个400坏的请求响应。
  当你运行该测试时,它应该成功通过。
  1.  $ go test -v
  2.   === RUN   TestGetJokeHandler
  3.   === RUN   TestGetJokeHandler/R7UfaahVfFd
  4.   === RUN   TestGetJokeHandler/173782
  5.   === RUN   TestGetJokeHandler/#00
  6.   --- PASS: TestGetJokeHandler (1.49s)
  7.       --- PASS: TestGetJokeHandler/R7UfaahVfFd (1.03s)
  8.       --- PASS: TestGetJokeHandler/173782 (0.47s)
  9.       --- PASS: TestGetJokeHandler/#00 (0.00s)
  10.   PASS
  11.   ok      github.com/ayoisaiah/random     1.498s
复制代码
 请注意,上面代码块中的测试向真正的API发出了HTTP请求。这样做会影响被测试代码的依赖性,这对单元测试代码来说是不好的做法。
  相反,我们应该模拟HTTP客户端。我们有几种不同的方法来模拟Go,下面我们就来探讨一下。
  Go中的嘲讽
  在Go中模拟HTTP客户端的一个相当简单的模式是创建一个自定义接口。我们的接口将定义一个函数中使用的方法,并根据函数的调用位置传递不同的实现。
  我们上面的HTTP客户端的自定义接口应该看起来像下面的代码块。
  1.  type HTTPClient interface {
  2.       Do(req *http.Request) (*http.Response, error)
  3.   }
复制代码
我们对getJoke() 的签名将看起来像下面的代码块。

  1.  func getJoke(client HTTPClient) http.HandlerFunc {
  2.       return func(w http.ResponseWriter, r *http.Request) {
  3.         // rest of the function
  4.       }
  5.   }
复制代码
getJoke() 处理程序的原始主体被移到返回值里面。client 变量声明被从主体中删除,而改用HTTPClient 接口。
  HTTPClient 接口封装了一个Do() 方法,它接受一个HTTP请求并返回一个HTTP响应和一个错误。
  当我们在main() 函数中调用getJoke() 时,我们需要提供一个HTTPClient 的具体实现。
  1. func main() {
  2.       mux := http.NewServeMux()
  3.       client := http.Client{
  4.           Timeout: 10 * time.Second,
  5.       }
  6.       mux.HandleFunc("/joke", getJoke(&client))
  7.       http.ListenAndServe(":1212", mux)
  8.   }
复制代码
http.Client 类型实现了HTTPClient 接口,所以程序继续调用随机爸爸笑话API。我们需要用一个不同的HTTPClient 实现来更新测试,它不会通过网络进行HTTP请求。
  首先,我们将创建一个HTTPClient 接口的模拟实现。
  1.  type MockClient struct {
  2.       DoFunc func(req *http.Request) (*http.Response, error)
  3.   }
  4.   func (m *MockClient) Do(req *http.Request) (*http.Response, error) {
  5.       return m.DoFunc(req)
  6.   }
复制代码
 在上面的代码块中,MockClient 结构通过提供Do 方法实现了HTTPClient 接口,该方法调用了DoFunc 属性。现在,当我们在测试中创建一个MockClient 的实例时,我们需要实现DoFunc 函数。

  1. func TestGetJokeHandler(t *testing.T) {
  2.       table := []struct {
  3.           id         string
  4.           statusCode int
  5.           body       string
  6.       }{
  7.           {"R7UfaahVfFd", 200, "My dog used to chase people on a bike a lot. It got so bad I had to take his bike away."},
  8.           {"173782", 404, `Joke with id "173782" not found`},
  9.           {"", 400, "Joke ID cannot be empty"},
  10.       }
  11.       for _, v := range table {
  12.           t.Run(v.id, func(t *testing.T) {
  13.               w := httptest.NewRecorder()
  14.               r := httptest.NewRequest(http.MethodGet, "/joke?id="+v.id, nil)
  15.               c := &MockClient{}
  16.               c.DoFunc = func(req *http.Request) (*http.Response, error) {
  17.                   return &http.Response{
  18.                       Body:       io.NopCloser(strings.NewReader(v.body)),
  19.                       StatusCode: v.statusCode,
  20.                   }, nil
  21.               }
  22.               getJoke(c)(w, r)
  23.               if w.Code != v.statusCode {
  24.                   t.Fatalf("Expected status code: %d, but got: %d", v.statusCode, w.Code)
  25.               }
  26.               body := strings.TrimSpace(w.Body.String())
  27.               if body != v.body {
  28.                   t.Fatalf("Expected body to be: '%s', but got: '%s'", v.body, body)
  29.               }
  30.           })
  31.       }
  32.   }
复制代码
在上面的代码片段中,DoFunc 为每个测试案例进行了调整,所以它返回一个自定义的响应。现在,我们已经避免了所有的网络调用,所以测试的通过率会快很多。

  1.  $ go test -v
  2.   === RUN   TestGetJokeHandler
  3.   === RUN   TestGetJokeHandler/R7UfaahVfFd
  4.   === RUN   TestGetJokeHandler/173782
  5.   === RUN   TestGetJokeHandler/#00
  6.   --- PASS: TestGetJokeHandler (0.00s)
  7.       --- PASS: TestGetJokeHandler/R7UfaahVfFd (0.00s)
  8.       --- PASS: TestGetJokeHandler/173782 (0.00s)
  9.       --- PASS: TestGetJokeHandler/#00 (0.00s)
  10.   PASS
  11.   ok      github.com/ayoisaiah/random     0.005s
复制代码
当你的处理程序依赖于另一个外部系统,如数据库时,你可以使用这个相同的原则。将处理程序与任何特定的实现解耦,允许你在测试中轻松模拟依赖关系,同时在你的应用程序的代码中保留真正的实现。











欢迎光临 51Testing软件测试论坛 (http://bbs.51testing.com/) Powered by Discuz! X3.2