Go中的高级单元测试模式(上)

发表于:2022-3-03 09:28

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:尼耳多    来源:稀土掘金

  一个好的开发者总是测试他们的代码,然而,普通的测试方法在某些情况下可能太简单了。根据项目的复杂程度,你可能需要运行高级测试来准确评估代码的性能。
  在这篇文章中,我们将研究Go中的一些测试模式,这将帮助你为任何项目编写有效的测试。我们将介绍嘲弄、测试夹具、测试助手和黄金文件等概念,你将看到如何在实际场景中应用每种技术
  要跟上这篇文章,你应该有Go中单元测试的知识。让我们开始吧!
  测试HTTP处理程序
  首先,让我们考虑一个常见的场景,测试HTTP处理程序。HTTP处理程序应该与它们的依赖关系松散地结合在一起,这样就可以很容易地隔离一个元素进行测试而不影响代码的其他部分。如果你的HTTP处理程序最初设计得很好,测试应该是相当简单的。
  检查状态代码
  让我们考虑一个基本的测试,检查以下HTTP处理程序的状态代码。
  func index(w http.ResponseWriter, r *http.Request) {
      w.WriteHeader(http.StatusOK)
  }

  上面的index() 处理程序应该为每个请求返回一个200 OK的响应。让我们用下面的测试来验证该处理程序的响应。
  func TestIndexHandler(t *testing.T) {
      w := httptest.NewRecorder()
      r := httptest.NewRequest(http.MethodGet, "/", nil)
      index(w, r)
      if w.Code != http.StatusOK {
          t.Errorf("Expected status: %d, but got: %d", http.StatusOK, w.Code)
      }
  }

  在上面的代码片段中,我们使用httptest 包来测试index() 处理器。我们返回了一个httptest.ResponseRecorder ,它通过NewRecorder() 方法实现了http.ResponseWriter 接口。http.ResponseWriter 记录了任何突变,使我们可以在测试中进行断言。
  我们还可以使用httptest.NewRequest() 方法创建一个HTTP请求。这样做可以指定处理程序所期望的请求类型,如请求方法、查询参数和响应体。在通过http.Header 类型获得http.Request 对象后,你还可以设置请求头。
  在用http.Request 对象和响应记录器调用index() 处理程序后,你可以使用Code 属性直接检查处理程序的响应。要对响应的其他属性进行断言,比如头或正文,你可以访问响应记录器上的适当方法或属性。
  $ go test -v
  === RUN   TestIndexHandler
  --- PASS: TestIndexHandler (0.00s)
  PASS
  ok      github.com/ayoisaiah/random 0.004s

  外部依赖性
  现在,让我们考虑另一种常见的情况,即我们的HTTP处理器对外部服务有依赖性。
  func getJoke(w http.ResponseWriter, r *http.Request) {
      u, err := url.Parse(r.URL.String())
      if err != nil {
          http.Error(w, err.Error(), http.StatusInternalServerError)
          return
      }
      jokeId := u.Query().Get("id")
      if jokeId == "" {
          http.Error(w, "Joke ID cannot be empty", http.StatusBadRequest)
          return
      }
      endpoint := "https://icanhazdadjoke.com/j/" + jokeId
      client := http.Client{
          Timeout: 10 * time.Second,
      }
      req, err := http.NewRequest(http.MethodGet, endpoint, nil)
      if err != nil {
          http.Error(w, err.Error(), http.StatusInternalServerError)
          return
      }
      req.Header.Set("Accept", "text/plain")
      resp, err := client.Do(req)
      if err != nil {
          http.Error(w, err.Error(), http.StatusInternalServerError)
          return
      }
      defer resp.Body.Close()
      b, err := ioutil.ReadAll(resp.Body)
      if err != nil {
          http.Error(w, err.Error(), http.StatusInternalServerError)
          return
      }
      if resp.StatusCode != http.StatusOK {
          http.Error(w, string(b), resp.StatusCode)
          return
      }
      w.Header().Set("Content-Type", "text/plain")
      w.WriteHeader(http.StatusOK)
      w.Write(b)
  }
  func main() {
      mux := http.NewServeMux()

  在上面的代码块中,getJoke 处理程序期望有一个id 查询参数,它用来从Random dad joke API中获取一个笑话。
  让我们为这个处理程序写一个测试。
  func TestGetJokeHandler(t *testing.T) {
      table := []struct {
          id         string
          statusCode int
          body       string
      }{
          {"R7UfaahVfFd", 200, "My dog used to chase people on a bike a lot. It got so bad I had to take his bike away."},
          {"173782", 404, `Joke with id "173782" not found`},
          {"", 400, "Joke ID cannot be empty"},
      }
      for _, v := range table {
          t.Run(v.id, func(t *testing.T) {
              w := httptest.NewRecorder()
              r := httptest.NewRequest(http.MethodGet, "/joke?id="+v.id, nil)
              getJoke(w, r)
              if w.Code != v.statusCode {
                  t.Fatalf("Expected status code: %d, but got: %d", v.statusCode, w.Code)
              }
              body := strings.TrimSpace(w.Body.String())
              if body != v.body {
                  t.Fatalf("Expected body to be: '%s', but got: '%s'", v.body, body)
              }
          })
      }
  }

  我们使用表格驱动测试来测试处理程序对一系列输入的影响。第一个输入是一个有效的Joke ID ,应该返回一个200 OK的响应。第二个是一个无效的ID,应该返回一个404响应。最后一个输入是一个空的ID,应该返回一个400坏的请求响应。
  当你运行该测试时,它应该成功通过。
  $ go test -v
  === RUN   TestGetJokeHandler
  === RUN   TestGetJokeHandler/R7UfaahVfFd
  === RUN   TestGetJokeHandler/173782
  === RUN   TestGetJokeHandler/#00
  --- PASS: TestGetJokeHandler (1.49s)
      --- PASS: TestGetJokeHandler/R7UfaahVfFd (1.03s)
      --- PASS: TestGetJokeHandler/173782 (0.47s)
      --- PASS: TestGetJokeHandler/#00 (0.00s)
  PASS
  ok      github.com/ayoisaiah/random     1.498s

  请注意,上面代码块中的测试向真正的API发出了HTTP请求。这样做会影响被测试代码的依赖性,这对单元测试代码来说是不好的做法。
  相反,我们应该模拟HTTP客户端。我们有几种不同的方法来模拟Go,下面我们就来探讨一下。
  Go中的嘲讽
  在Go中模拟HTTP客户端的一个相当简单的模式是创建一个自定义接口。我们的接口将定义一个函数中使用的方法,并根据函数的调用位置传递不同的实现。
  我们上面的HTTP客户端的自定义接口应该看起来像下面的代码块。
  type HTTPClient interface {
      Do(req *http.Request) (*http.Response, error)
  }

  我们对getJoke() 的签名将看起来像下面的代码块。
  func getJoke(client HTTPClient) http.HandlerFunc {
      return func(w http.ResponseWriter, r *http.Request) {
        // rest of the function
      }
  }

  getJoke() 处理程序的原始主体被移到返回值里面。client 变量声明被从主体中删除,而改用HTTPClient 接口。
  HTTPClient 接口封装了一个Do() 方法,它接受一个HTTP请求并返回一个HTTP响应和一个错误。
  当我们在main() 函数中调用getJoke() 时,我们需要提供一个HTTPClient 的具体实现。
  func main() {
      mux := http.NewServeMux()
      client := http.Client{
          Timeout: 10 * time.Second,
      }
      mux.HandleFunc("/joke", getJoke(&client))
      http.ListenAndServe(":1212", mux)
  }

  http.Client 类型实现了HTTPClient 接口,所以程序继续调用随机爸爸笑话API。我们需要用一个不同的HTTPClient 实现来更新测试,它不会通过网络进行HTTP请求。
  首先,我们将创建一个HTTPClient 接口的模拟实现。
  type MockClient struct {
      DoFunc func(req *http.Request) (*http.Response, error)
  }
  func (m *MockClient) Do(req *http.Request) (*http.Response, error) {
      return m.DoFunc(req)
  }

  在上面的代码块中,MockClient 结构通过提供Do 方法实现了HTTPClient 接口,该方法调用了DoFunc 属性。现在,当我们在测试中创建一个MockClient 的实例时,我们需要实现DoFunc 函数。
  func TestGetJokeHandler(t *testing.T) {
      table := []struct {
          id         string
          statusCode int
          body       string
      }{
          {"R7UfaahVfFd", 200, "My dog used to chase people on a bike a lot. It got so bad I had to take his bike away."},
          {"173782", 404, `Joke with id "173782" not found`},
          {"", 400, "Joke ID cannot be empty"},
      }
      for _, v := range table {
          t.Run(v.id, func(t *testing.T) {
              w := httptest.NewRecorder()
              r := httptest.NewRequest(http.MethodGet, "/joke?id="+v.id, nil)
              c := &MockClient{}
              c.DoFunc = func(req *http.Request) (*http.Response, error) {
                  return &http.Response{
                      Body:       io.NopCloser(strings.NewReader(v.body)),
                      StatusCode: v.statusCode,
                  }, nil
              }
              getJoke(c)(w, r)
              if w.Code != v.statusCode {
                  t.Fatalf("Expected status code: %d, but got: %d", v.statusCode, w.Code)
              }
              body := strings.TrimSpace(w.Body.String())
              if body != v.body {
                  t.Fatalf("Expected body to be: '%s', but got: '%s'", v.body, body)
              }
          })
      }
  }

  在上面的代码片段中,DoFunc 为每个测试案例进行了调整,所以它返回一个自定义的响应。现在,我们已经避免了所有的网络调用,所以测试的通过率会快很多。
  $ go test -v
  === RUN   TestGetJokeHandler
  === RUN   TestGetJokeHandler/R7UfaahVfFd
  === RUN   TestGetJokeHandler/173782
  === RUN   TestGetJokeHandler/#00
  --- PASS: TestGetJokeHandler (0.00s)
      --- PASS: TestGetJokeHandler/R7UfaahVfFd (0.00s)
      --- PASS: TestGetJokeHandler/173782 (0.00s)
      --- PASS: TestGetJokeHandler/#00 (0.00s)
  PASS
  ok      github.com/ayoisaiah/random     0.005s

  当你的处理程序依赖于另一个外部系统,如数据库时,你可以使用这个相同的原则。将处理程序与任何特定的实现解耦,允许你在测试中轻松模拟依赖关系,同时在你的应用程序的代码中保留真正的实现。

  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号