Prav
Prav

Reputation: 2884

How to stub a method inside another

I'm writing a web app that will send requests to a third-party service to do some calculations, and send it back to the fronted.

Here are the relevant parts for the test I'm trying to writer.

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
}

I only want to test the GetBankAccounts() method so I want to stub the ClientDo, but I'm at a loss on how to do that. Here's what I have so far with my test case.

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)
}

The ClintDo still pointing to the actual method on the helper.go, how can I make it use the on in the test?


Update: I've also tried the following and this doesn't work either, it still sends the request to actual third-party service.

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
}

Update 2

I was able to make some progress from @dm03514 's answer but unfortunately, now I'm getting nil pointer exceptions on the test but not on actual code.

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]

Upvotes: 0

Views: 224

Answers (1)

dm03514
dm03514

Reputation: 55932

There are two common ways to achieve this:

  • Dependency Injection using interfaces (your example)
  • Custom http.Transport, which has a hook you can override in your unit tests

It looks like you're close on the interface approach, and are lacking an explicit way to configure the concrete implementation. Consider an interface similiar to your ClientDo:

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

Dependency injection has the caller configure depedencies and pass them into the resources that actually invoke those dependencies. In this case your ClientResponse struct would have a reference to a HTTPDoer:

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

This allows the caller to configure the concrete implementation that ClientResponse will invoke. In production this will be the actual http.Client but in test it could be anything that implements the Do function.

type StubDoer struct {}

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

The unit test could configure the StubDoer, then invoke GetBankAccounts and then make asserstion:

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

The reason it's called Dependency Injection is that the caller initializes the resource (StubDoer) and then provides that resource to the target (ClientResponse). ClientResponse knows nothing about the concrete implementation of HTTPDoer, only that it adheres to the interface!


I wrote a blog post that details dependency injection in the context of unit tests.

Upvotes: 1

Related Questions