如何在另一个内部存根方法

问题描述 投票:1回答:1

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

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

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]
unit-testing go testing mocking stubbing
1个回答
1
投票

有两种常见的实现方法:

  • 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的具体实现一无所知,只知道它遵守接口!


I wrote a blog post,详细介绍了单元测试中的依赖项注入。

© www.soinside.com 2019 - 2024. All rights reserved.