【问题标题】:Override Go Method in Tests在测试中覆盖 Go 方法
【发布时间】:2018-11-05 18:46:57
【问题描述】:

所以我有这个Client 结构,它有一个方法UserByID,它向User 的端点发出HTTP 请求。我想对该函数进行单元测试,但也不想在函数c.Request 中发出实际的 HTTP 请求。我想用我可以控制的响应和错误来存根该函数。

func (c Client) UserByID(id string) (u User, err error) {
  v := url.Values{}
  v.Set("id", id)
  opts := Request{
    HTTP: http.Request{
        Method: http.MethodGet,
        Form:   v,
    },
    URL: 'some/endpoint/users',
  }
  resp, err := c.Request(opts)
  err = json.Unmarshal(resp, &u)
  return
}

这是存根的样子:

type mockClient struct {
  Client
  fakeUser  User
  fakeError error
}

func (mc mockClient) Request(opts Request) (resp []byte, err error) {
  resp, err = json.Marshal(mc.fakeUser)
  err = mc.fakeError
  return
}

在一次测试中,我有类似的东西:

client := mockClient{
  fakeUser: User{},
  fakeError: nil,
}
user, err := client.UserByID(c.id)

然后我可以断言来自client.UserByID 的返回值。在此示例中,我试图覆盖 client.Request 函数,但我了解 Go 不是继承类型的语言。在我的测试中,我的 mockClient.Request 函数没有被调用。原来的client.Request 仍在调用中。

然后我假设我的方法不正确。如何测试client.UserByID 而不实际调用其中的真实client.Request 函数?我的方法的设计应该不同吗?

【问题讨论】:

  • 不要模拟你的方法——模拟你的方法调用的函数。所以在这种情况下,模拟c.Request()--或模拟它与之对话的服务器(在 Go 中启动一个可以发送实际 HTTP 响应供您的测试使用的测试服务器是微不足道的)。
  • Java 式的单元测试尝试在 Go 中注定要失败。不要这样做。要么调用一个实际的 HTTP 服务器(微不足道的),要么分离这个功能。甚至永远不要在 Go 中尝试传统的 OOP 技术,它们都会失败。

标签: unit-testing oop testing go


【解决方案1】:

为了完成您的需要,您可以稍微重新构建您的代码。

您可以在此处找到完整的工作示例:https://play.golang.org/p/VoO4M4U0YcA

下面是解释。

首先,在你的包中声明一个变量函数来封装HTTP请求的实际发出:

var MakeRequest = func(opts Request) (resp []byte, err error) {
    // make the request, return response and error, etc
}

然后,在您的Client 中使用该函数发出请求:

func (c Client) Request(opts Request) (resp []byte, err error) {
    return MakeRequest(opts)
}

这样,当你实际使用客户端时,它会按预期发出 HTTP 请求。

但是当你需要测试时,你可以为MakeRequest 函数分配一个模拟函数,这样你就可以控制它的行为:

// define a mock requester for your test

type mockRequester struct {
    fakeUser  User
    fakeError error
}

func (mc mockRequester) Request(opts Request) (resp []byte, err error) {
    resp, err = json.Marshal(mc.fakeUser)
    err = mc.fakeError
    return
}

// to use it, you can just point `MakeRequest` to the mock object function

mockRequester := mockRequester{
    fakeUser:  User{ ID: "fake" },
    fakeError: nil,
}
MakeRequest = mockRequester.Request

【讨论】:

    【解决方案2】:

    然后我假设我的方法不正确。

    您的描述准确无误!即使您是 mockClient 中的 embedding Client,当您调用 client.UserByID(c.id) 时,也会查看 mockClient 并看到从 Client 提取的方法。它最终使Client!!!是UserByID 的接收者,而不是mockClient。你可以在这里看到:

    func (c Client) UserByID(id string) (u User, err error)

    一旦Client 是接收器 resp, err := c.Request(opts) 被上面的Client 接收器调用,而不是你观察到的mockClient


    c.Request 引入seam 的一种方法是,在您的Client 结构上创建Request 一个标注方法,您可以提供自定义实现以用于单元测试。

    type Client struct {
        Request func(opts Request) (resp []byte, err error) 
    }
    

    以上内容应该有助于将 Client 与 Request 实现分离。它所说的只是Request 将是一个函数,它接受一些带有一些返回值的参数,允许您根据您是在生产还是测试中替换不同的函数。现在,在 Client 的公开初始化期间,您可以提供 Request 的真实实现,而在单元测试中,您可以提供假实现。

    type mockRequester struct {
      fakeUser  User
      fakeError error
    }
    
    func (mc mockRequester) Request(opts Request) (resp []byte, err error) {
      resp, err = json.Marshal(mc.fakeUser)
      err = mc.fakeError
      return
    }
    
    mr := mockRequester{...}
    c := Client{  
      Request: mr.Request,
    }
    

    这有其自身的权衡,因为您可能会在 Request 标注函数中失去作为指针接收器的客户端。

    Callout 的另一个很酷的部分是它为您提供了另一种封装选项。假设将来您想提供某种指数退避或重试。它将允许您为Client 提供更智能的Request 方法,而无需更改Client

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-08-12
      • 2012-12-07
      • 1970-01-01
      • 2020-02-21
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多