代码之家  ›  专栏  ›  技术社区  ›  Prav

Golang如何将一个方法插入另一个方法中?

  •  0
  • Prav  · 技术社区  · 6 年前

    我正在编写一个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
    }
    

    帮帮忙

    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 仍指向实际方法 帮帮忙 ,我如何在测试中使用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)
    }
    

    帮帮忙

    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
    }
    

    帮帮忙

    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]
    
    0 回复  |  直到 6 年前
        1
  •  1
  •   dm03514    6 年前

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

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

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

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

    依赖注入让调用者配置依赖关系,并将其传递给实际调用这些依赖关系的资源。在这种情况下,你的 ClientResponse struct将引用 HTTPDoer :

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

    这允许调用者配置具体实现 客户响应 将调用。在生产中,这将是实际的 http.Client 但在测试中,它可以是任何实现 Do 功能。

    type StubDoer struct {}
    
    func (s *StubDoer) Do(....)
    

    单元测试可以配置 StubDoer ,然后调用 GetBankAccounts 然后进行资产评估:

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

    之所以称之为依赖注入,是因为调用者初始化了资源( StubDoer )然后将该资源提供给目标( 客户响应 ). 客户响应 对具体实施一无所知 HTTPDoer ,只是它遵守界面!


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