如何在另一个方法中存根

How to stub a method inside another

我正在编写一个网络应用程序,它将向第三方服务发送请求以进行一些计算,然后将其发送回前台。

这是我要编写的测试的相关部分。

client.go

func (c *ClientResponse) GetBankAccounts() (*BankAccounts, *RequestError) {
    req, _ := http.NewRequest("GET", app.BuildUrl("bank_accounts"), nil)
    params := req.URL.Query()
    params.Add("view", "standard_bank_accounts")
    req.URL.RawQuery = params.Encode()

    c.ClientDo(req)
    if c.Err.Errors != nil {
        return nil, c.Err
    }

    bankAccounts := new(BankAccounts)
    defer c.Response.Body.Close()
    if err := json.NewDecoder(c.Response.Body).Decode(bankAccounts); err != nil {
        return nil, &RequestError{Errors: &Errors{Error{Message: "failed to decode Bank Account response body"}}}
    }

    return bankAccounts, nil
}

helper.go

type ClientResponse struct {
    Response *http.Response
    Err      *RequestError
}

type ClientI interface {
    ClintDo(req *http.Request) (*http.Response, *RequestError)
}

func (c *ClientResponse) ClientDo(req *http.Request) {
    //Do some authentication with third-party service

    errResp := *new(RequestError)
    client := http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        // Here I'm repourposing the third-party service's error response mapping
        errResp.Errors.Error.Message = "internal server error. failed create client.Do"
    }
    c.Response = resp
    c.Err = &errResp
}

我只想测试 GetBankAccounts() 方法,所以我想存根 ClientDo,但我不知道该怎么做。到目前为止,这是我的测试用例。

client_test.go

type StubClientI interface {
    ClintDo(req *http.Request) (*http.Response, *RequestError)
}

type StubClientResponse struct {}

func (c *StubClientResponse) ClientDo(req *http.Request) (*http.Response, *RequestError) {
    return nil, nil
}

func TestGetBankAccounts(t *testing.T) {
    cr := new(ClientResponse)
    accounts, err := cr.GetBankAccounts()
    if err != nil {
        t.Fatal(err.Errors)
    }
    t.Log(accounts)
}

ClintDo仍然指向helper.go上的实际方法,如何让它在测试中使用on?


更新: 我也尝试了以下方法,但这也不起作用,它仍然将请求发送到实际的第三方服务。

client_test.go

func TestGetBankAccounts(t *testing.T) {
    mux := http.NewServeMux()
    mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        fmt.Fprint(w, toJson(append(BankAccounts{}.BankAccounts, BankAccount{
            Url:  "https://foo.bar/v2/bank_accounts/1234",
            Name: "Test Bank",
        })))
    }))
    server := httptest.NewServer(mux)
    cr := new(ClientResponse)
    cr.Client = server.Client()
    accounts, err := cr.GetBankAccounts()
    if err != nil {
        t.Fatal(err.Errors)
    }
    t.Log(accounts)
}

helper.go

type ClientResponse struct {
    Client   *http.Client
    Response *http.Response
    Err      *RequestError
}

type ClientI interface {
    ClintDo(req *http.Request) (*http.Response, *RequestError)
}

func (c *ClientResponse) ClientDo(req *http.Request) {
    //Do some authentication with third-party service

    errResp := *new(RequestError)
    client := c.Client
    resp, err := client.Do(req)
    if err != nil {
        // Here I'm repourposing the third-party service's error response mapping
        errResp.Errors.Error.Message = "internal server error. failed create client.Do"
    }
    c.Response = resp
    c.Err = &errResp
}

更新 2

我能够从 @dm03514 的回答中取得一些进展,但不幸的是,现在我在测试中得到了 nil 指针异常,但在实际代码中却没有。

client.go

func (c *ClientResponse) GetBankAccounts() (*BankAccounts, *RequestError) {
    req, _ := http.NewRequest("GET", app.BuildUrl("bank_accounts"), nil)
    params := req.URL.Query()
    params.Add("view", "standard_bank_accounts")
    req.URL.RawQuery = params.Encode()

    //cr := new(ClientResponse)
    c.HTTPDoer.ClientDo(req)
    // Panic occurs here
    if c.Err.Errors != nil {
        return nil, c.Err
    }

    bankAccounts := new(BankAccounts)
    defer c.Response.Body.Close()
    if err := json.NewDecoder(c.Response.Body).Decode(bankAccounts); err != nil {
        return nil, &RequestError{Errors: &Errors{Error{Message: "failed to decode Bank Account response body"}}}
    }

    return bankAccounts, nil
}

helper.go

type ClientResponse struct {
    Response *http.Response
    Err      *RequestError
    HTTPDoer HTTPDoer
}

type HTTPDoer interface {
    //Do(req *http.Request) (*http.Response, *RequestError)
    ClientDo(req *http.Request)
}

type ClientI interface {
}

func (c *ClientResponse) ClientDo(req *http.Request) {
  // This method hasn't changed
  ....
}

client_test.go

type StubDoer struct {
    *ClientResponse
}

func (s *StubDoer) ClientDo(req *http.Request) {
    s.Response = &http.Response{
        StatusCode: 200,
        Body:       nil,
    }
    s.Err = nil
}

func TestGetBankAccounts(t *testing.T) {
    sd := new(StubDoer)
    cr := new(ClientResponse)
    cr.HTTPDoer = HTTPDoer(sd)
    accounts, err := cr.GetBankAccounts()
    if err != nil {
        t.Fatal(err.Errors)
    }
    t.Log(accounts)
}
=== RUN   TestGetBankAccounts
--- FAIL: TestGetBankAccounts (0.00s)
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
    panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x12aae69]

有两种常见的方法可以实现这一点:

  • Dependency Injection 使用接口(你的例子)
  • 自定义 http.Transport,它有一个挂钩,您可以在单元测试中覆盖它

看起来您已经接近接口方法,并且缺少配置具体实现的明确方法。考虑一个类似于您的 ClientDo 的界面:

type HTTPDoer interface {
  Do func(req *http.Request) (*http.Response, *RequestError)
}

依赖注入让调用者配置依赖项并将它们传递到实际调用这些依赖项的资源中。在这种情况下,您的 ClientResponse 结构将引用 HTTPDoer:

type ClientResponse struct {
    Response *http.Response
    Err      *RequestError
    HTTPDoer HTTPDoer
}

这允许调用者配置 ClientResponse 将调用的具体实现。在生产中这将是实际的 http.Client 但在测试中它可以是实现 Do 函数的任何东西。

type StubDoer struct {}

func (s *StubDoer) Do(....)

单元测试可以配置StubDoer,然后调用GetBankAccounts,然后断言:

sd := &StubDoer{...}
cr := ClientResponse{
   HTTPDoer: sd,
}
accounts, err := cr.GetBankAccounts()
// assertions

它被称为依赖注入的原因是调用者初始化资源(StubDoer),然后将该资源提供给目标(ClientResponse)。 ClientResponseHTTPDoer 的具体实现一无所知,只知道它遵循接口!


wrote a blog post 详细介绍了单元测试上下文中的依赖注入。