首页 > 解决方案 > 如何在另一个方法中存根

问题描述

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

这是我正在尝试编写的测试的相关部分。

客户端.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
}

助手.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)
}

助手.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 指针异常,但在实际代码中却没有。

客户端.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
}

助手.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]

标签: unit-testinggotestingmockingstubbing

解决方案


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

看起来您已经接近接口方法,并且缺乏配置具体实现的显式方法。考虑一个与您的 ClientDo 类似的接口:

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

依赖注入让调用者配置依赖并将它们传递到实际调用这些依赖的资源中。在这种情况下,您的ClientResponse结构将引用 a 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)。ClientResponse对 的具体实现一无所知HTTPDoer,只知道它遵守接口!


写了一篇博客文章,详细介绍了单元测试上下文中的依赖注入。


推荐阅读